diff --git a/src/features/tool-shell/tool-action-bar.tsx b/src/features/tool-shell/tool-action-bar.tsx index 674f3463..b70a7ff5 100644 --- a/src/features/tool-shell/tool-action-bar.tsx +++ b/src/features/tool-shell/tool-action-bar.tsx @@ -140,7 +140,7 @@ function getResultAnnouncement(action: ToolAction, result: ToolActionResult | vo const message = result.message || (result.status === "success" ? formatActionStatus(t.common.action_status_success, action.label, "completed") : formatActionStatus(t.common.action_status_failed, action.label, "failed")) - return result.description ? `${message}. ${result.description}` : message + return result.description ? `${message}${/[.!?]$/.test(message) ? "" : "."} ${result.description}` : message } return formatActionStatus(t.common.action_status_success, action.label, "completed") @@ -219,11 +219,16 @@ export function ToolActionBar({ setLastActionStatus((current) => ({ ...current, [action.id]: "pending" })) setActionAnnouncement(formatActionStatus(t.common.action_status_pending, action.label, "in progress")) const awaitedResult = await maybeResult - setLastActionStatus((current) => ({ - ...current, - [action.id]: isActionResult(awaitedResult) ? awaitedResult.status : "success", - })) - setActionAnnouncement(getResultAnnouncement(action, awaitedResult, t)) + if (isActionResult(awaitedResult)) { + setLastActionStatus((current) => ({ + ...current, + [action.id]: awaitedResult.status, + })) + setActionAnnouncement(getResultAnnouncement(action, awaitedResult, t)) + } else { + setLastActionStatus((current) => ({ ...current, [action.id]: "idle" })) + setActionAnnouncement("") + } } catch { setLastActionStatus((current) => ({ ...current, [action.id]: "failed" })) setActionAnnouncement(formatActionStatus(t.common.action_status_failed, action.label, "failed")) diff --git a/src/features/tool-templates/focused-hash-tool-page.tsx b/src/features/tool-templates/focused-hash-tool-page.tsx index aa31e030..a79f7b5a 100644 --- a/src/features/tool-templates/focused-hash-tool-page.tsx +++ b/src/features/tool-templates/focused-hash-tool-page.tsx @@ -245,7 +245,7 @@ export function FocusedHashToolPage({ const actions: ToolAction[] = [ { id: "sample", label: t.common.sample, icon: TestTube2, onClick: handleSample }, { id: "reset", label: t.common.reset, icon: Eraser, onClick: handleReset }, - { id: "copy", label: t.common.copy, icon: Copy, onClick: () => void handleCopy() }, + { id: "copy", label: t.common.copy, icon: Copy, onClick: handleCopy }, { id: "download", label: t.common.download, icon: Download, onClick: handleDownload, disabled: !canDownload }, ] diff --git a/src/features/tools/ai-color-palette-generator/page.tsx b/src/features/tools/ai-color-palette-generator/page.tsx index c067ffc3..6a16ed65 100644 --- a/src/features/tools/ai-color-palette-generator/page.tsx +++ b/src/features/tools/ai-color-palette-generator/page.tsx @@ -77,7 +77,7 @@ export function AiColorPaletteGeneratorPage() { const actions: ToolAction[] = [ { id: "sample", label: t.common.sample, icon: TestTube2, onClick: handleSample }, { id: "reset", label: t.common.reset, icon: Eraser, onClick: handleReset }, - { id: "copy", label: t.common.copy, icon: Copy, onClick: () => void handleCopy() }, + { id: "copy", label: t.common.copy, icon: Copy, onClick: handleCopy }, { id: "download", label: t.common.download, icon: Download, onClick: handleDownload }, ] diff --git a/src/features/tools/barcode-generator/page.tsx b/src/features/tools/barcode-generator/page.tsx index 9923a08f..215ba3d4 100644 --- a/src/features/tools/barcode-generator/page.tsx +++ b/src/features/tools/barcode-generator/page.tsx @@ -200,9 +200,9 @@ export function BarcodeGeneratorPage() { { id: "sample", label: t.common.sample, icon: TestTube2, onClick: handleSample }, { id: "generate", label: toolT.generate_action, icon: Play, onClick: handleGenerate }, { id: "reset", label: t.common.reset, icon: Eraser, onClick: handleReset }, - { id: "copy", label: t.common.copy, icon: Copy, onClick: () => void handleCopy() }, + { id: "copy", label: t.common.copy, icon: Copy, onClick: handleCopy }, { id: "svg", label: "SVG", icon: Download, onClick: handleDownloadSvg, disabled: !canExport }, - { id: "png", label: "PNG", icon: Download, onClick: () => void handleDownloadPng(), disabled: !canExport }, + { id: "png", label: "PNG", icon: Download, onClick: handleDownloadPng, disabled: !canExport }, ] return ( diff --git a/src/features/tools/bionic-reading-converter/page.tsx b/src/features/tools/bionic-reading-converter/page.tsx index d7a231ac..07c3c49a 100644 --- a/src/features/tools/bionic-reading-converter/page.tsx +++ b/src/features/tools/bionic-reading-converter/page.tsx @@ -128,7 +128,7 @@ export function BionicReadingConverterPage() { const actions: ToolAction[] = [ { id: "sample", label: t.common.sample, icon: TestTube2, onClick: handleSample }, { id: "clear", label: t.common.clear, icon: Eraser, onClick: handleClear }, - { id: "copy", label: t.common.copy, icon: Copy, onClick: () => void handleCopy(), disabled: !output.trim() }, + { id: "copy", label: t.common.copy, icon: Copy, onClick: handleCopy, disabled: !output.trim() }, { id: "download", label: t.common.download, icon: Download, onClick: handleDownload, disabled: !output.trim() }, ] diff --git a/src/features/tools/certificate-decoder/page.tsx b/src/features/tools/certificate-decoder/page.tsx index 7abcb0d3..70a251ae 100644 --- a/src/features/tools/certificate-decoder/page.tsx +++ b/src/features/tools/certificate-decoder/page.tsx @@ -289,7 +289,10 @@ export function CertificateDecoderPage() { try { const text = await navigator.clipboard.readText() setPem(text) - } catch { /* ignore */ } + return { status: "success" as const, message: t.common.paste } + } catch { + return { status: "failed" as const, message: t.common.action_disabled_unavailable } + } } const actions: ToolAction[] = [ @@ -297,9 +300,7 @@ export function CertificateDecoderPage() { id: "paste", label: t.common.paste, icon: Clipboard, - onClick: () => { - void handlePaste() - }, + onClick: handlePaste, }, { id: "clear", diff --git a/src/features/tools/cidr-subnet-calculator/page.tsx b/src/features/tools/cidr-subnet-calculator/page.tsx index ad90c3c8..2eab70b4 100644 --- a/src/features/tools/cidr-subnet-calculator/page.tsx +++ b/src/features/tools/cidr-subnet-calculator/page.tsx @@ -81,9 +81,7 @@ export function CidrSubnetCalculatorPage() { label: t.common.copy, icon: Copy, disabled: !result, - onClick: () => { - void handleCopySummary() - }, + onClick: handleCopySummary, }, ] diff --git a/src/features/tools/color-mixer/page.tsx b/src/features/tools/color-mixer/page.tsx index 28453e09..bca5e017 100644 --- a/src/features/tools/color-mixer/page.tsx +++ b/src/features/tools/color-mixer/page.tsx @@ -86,7 +86,7 @@ export function ColorMixerPage() { const actions: ToolAction[] = [ { id: "sample", label: t.common.sample, icon: TestTube2, onClick: handleSample }, { id: "reset", label: t.common.reset, icon: Eraser, onClick: handleReset }, - { id: "copy", label: t.common.copy, icon: Copy, onClick: () => void handleCopy() }, + { id: "copy", label: t.common.copy, icon: Copy, onClick: handleCopy }, { id: "download", label: t.common.download, icon: Download, onClick: handleDownload }, ] diff --git a/src/features/tools/color-shades-generator/page.tsx b/src/features/tools/color-shades-generator/page.tsx index 7358af0f..3896922f 100644 --- a/src/features/tools/color-shades-generator/page.tsx +++ b/src/features/tools/color-shades-generator/page.tsx @@ -71,7 +71,7 @@ export function ColorShadesGeneratorPage() { const actions: ToolAction[] = [ { id: "sample", label: t.common.sample, icon: TestTube2, onClick: handleSample }, { id: "reset", label: t.common.reset, icon: Eraser, onClick: handleReset }, - { id: "copy", label: t.common.copy, icon: Copy, onClick: () => void handleCopy() }, + { id: "copy", label: t.common.copy, icon: Copy, onClick: handleCopy }, { id: "download", label: t.common.download, icon: Download, onClick: handleDownload }, ] diff --git a/src/features/tools/css-background-pattern-generator/page.tsx b/src/features/tools/css-background-pattern-generator/page.tsx index db400a05..4c58428c 100644 --- a/src/features/tools/css-background-pattern-generator/page.tsx +++ b/src/features/tools/css-background-pattern-generator/page.tsx @@ -104,7 +104,7 @@ export function CssBackgroundPatternGeneratorPage() { id: "copy", label: t.common.copy, icon: Copy, - onClick: () => void handleCopy(), + onClick: handleCopy, }, { id: "download", diff --git a/src/features/tools/css-border-radius-generator/page.tsx b/src/features/tools/css-border-radius-generator/page.tsx index eb563781..b4e0e49c 100644 --- a/src/features/tools/css-border-radius-generator/page.tsx +++ b/src/features/tools/css-border-radius-generator/page.tsx @@ -105,7 +105,7 @@ export function CssBorderRadiusGeneratorPage() { id: "copy", label: t.common.copy, icon: Copy, - onClick: () => void handleCopy(), + onClick: handleCopy, }, { id: "download", diff --git a/src/features/tools/css-box-shadow-generator/page.tsx b/src/features/tools/css-box-shadow-generator/page.tsx index 0d591b58..6558c346 100644 --- a/src/features/tools/css-box-shadow-generator/page.tsx +++ b/src/features/tools/css-box-shadow-generator/page.tsx @@ -90,7 +90,7 @@ export function CssBoxShadowGeneratorPage() { id: "copy", label: t.common.copy, icon: Copy, - onClick: () => void handleCopy(), + onClick: handleCopy, }, { id: "download", diff --git a/src/features/tools/css-checkbox-generator/page.tsx b/src/features/tools/css-checkbox-generator/page.tsx index 6dd9521a..5ac7d1c2 100644 --- a/src/features/tools/css-checkbox-generator/page.tsx +++ b/src/features/tools/css-checkbox-generator/page.tsx @@ -98,7 +98,7 @@ export function CssCheckboxGeneratorPage() { const actions: ToolAction[] = [ { id: "sample", label: t.common.sample, icon: TestTube2, onClick: handleSample }, { id: "reset", label: t.common.reset, icon: Eraser, onClick: handleReset }, - { id: "copy", label: t.common.copy, icon: Copy, onClick: () => void handleCopy() }, + { id: "copy", label: t.common.copy, icon: Copy, onClick: handleCopy }, { id: "download", label: t.common.download, icon: Download, onClick: handleDownload }, ] diff --git a/src/features/tools/css-clip-path-generator/page.tsx b/src/features/tools/css-clip-path-generator/page.tsx index 9dc0ba0b..3f87153f 100644 --- a/src/features/tools/css-clip-path-generator/page.tsx +++ b/src/features/tools/css-clip-path-generator/page.tsx @@ -71,7 +71,7 @@ export function CssClipPathGeneratorPage() { const actions: ToolAction[] = [ { id: "sample", label: t.common.sample, icon: TestTube2, onClick: handleSample }, { id: "reset", label: t.common.reset, icon: Eraser, onClick: handleReset }, - { id: "copy", label: t.common.copy, icon: Copy, onClick: () => void handleCopy() }, + { id: "copy", label: t.common.copy, icon: Copy, onClick: handleCopy }, { id: "download", label: t.common.download, icon: Download, onClick: handleDownload }, ] diff --git a/src/features/tools/css-cubic-bezier-generator/page.tsx b/src/features/tools/css-cubic-bezier-generator/page.tsx index 0bba0b5d..72e63779 100644 --- a/src/features/tools/css-cubic-bezier-generator/page.tsx +++ b/src/features/tools/css-cubic-bezier-generator/page.tsx @@ -70,7 +70,7 @@ export function CssCubicBezierGeneratorPage() { { id: "sample", label: t.common.sample, icon: TestTube2, onClick: handleSample }, { id: "run", label: toolT.run_action, icon: Play, onClick: () => setRun((v) => !v) }, { id: "reset", label: t.common.reset, icon: Eraser, onClick: handleReset }, - { id: "copy", label: t.common.copy, icon: Copy, onClick: () => void handleCopy() }, + { id: "copy", label: t.common.copy, icon: Copy, onClick: handleCopy }, { id: "download", label: t.common.download, icon: Download, onClick: handleDownload }, ] diff --git a/src/features/tools/css-glassmorphism-generator/page.tsx b/src/features/tools/css-glassmorphism-generator/page.tsx index 4032254e..4379b8ac 100644 --- a/src/features/tools/css-glassmorphism-generator/page.tsx +++ b/src/features/tools/css-glassmorphism-generator/page.tsx @@ -97,7 +97,7 @@ export function CssGlassmorphismGeneratorPage() { const actions: ToolAction[] = [ { id: "sample", label: t.common.sample, icon: TestTube2, onClick: handleSample }, { id: "reset", label: t.common.reset, icon: Eraser, onClick: handleReset }, - { id: "copy", label: t.common.copy, icon: Copy, onClick: () => void handleCopy() }, + { id: "copy", label: t.common.copy, icon: Copy, onClick: handleCopy }, { id: "download", label: t.common.download, icon: Download, onClick: handleDownload }, ] diff --git a/src/features/tools/css-gradient-generator/page.tsx b/src/features/tools/css-gradient-generator/page.tsx index e1e58ccb..ec19d5fa 100644 --- a/src/features/tools/css-gradient-generator/page.tsx +++ b/src/features/tools/css-gradient-generator/page.tsx @@ -96,7 +96,7 @@ export function CssGradientGeneratorPage() { const actions: ToolAction[] = [ { id: "sample", label: t.common.sample, icon: TestTube2, onClick: handleSample }, { id: "reset", label: t.common.reset, icon: Eraser, onClick: handleReset }, - { id: "copy", label: t.common.copy, icon: Copy, onClick: () => void handleCopy() }, + { id: "copy", label: t.common.copy, icon: Copy, onClick: handleCopy }, { id: "download", label: t.common.download, icon: Download, onClick: handleDownload }, ] diff --git a/src/features/tools/css-loader-generator/page.tsx b/src/features/tools/css-loader-generator/page.tsx index e34e4c86..7c1575e6 100644 --- a/src/features/tools/css-loader-generator/page.tsx +++ b/src/features/tools/css-loader-generator/page.tsx @@ -76,7 +76,7 @@ export function CssLoaderGeneratorPage() { const actions: ToolAction[] = [ { id: "sample", label: t.common.sample, icon: TestTube2, onClick: handleSample }, { id: "reset", label: t.common.reset, icon: Eraser, onClick: handleReset }, - { id: "copy", label: t.common.copy, icon: Copy, onClick: () => void handleCopy() }, + { id: "copy", label: t.common.copy, icon: Copy, onClick: handleCopy }, { id: "download", label: t.common.download, icon: Download, onClick: handleDownload }, ] diff --git a/src/features/tools/css-switch-generator/page.tsx b/src/features/tools/css-switch-generator/page.tsx index 5c2889d5..d6a8c0b7 100644 --- a/src/features/tools/css-switch-generator/page.tsx +++ b/src/features/tools/css-switch-generator/page.tsx @@ -106,7 +106,7 @@ export function CssSwitchGeneratorPage() { const actions: ToolAction[] = [ { id: "sample", label: t.common.sample, icon: TestTube2, onClick: handleSample }, { id: "reset", label: t.common.reset, icon: Eraser, onClick: handleReset }, - { id: "copy", label: t.common.copy, icon: Copy, onClick: () => void handleCopy() }, + { id: "copy", label: t.common.copy, icon: Copy, onClick: handleCopy }, { id: "download", label: t.common.download, icon: Download, onClick: handleDownload }, ] diff --git a/src/features/tools/css-text-glitch-effect-generator/page.tsx b/src/features/tools/css-text-glitch-effect-generator/page.tsx index e9e5b6d3..96bb2628 100644 --- a/src/features/tools/css-text-glitch-effect-generator/page.tsx +++ b/src/features/tools/css-text-glitch-effect-generator/page.tsx @@ -89,7 +89,7 @@ export function CssTextGlitchEffectGeneratorPage() { const actions: ToolAction[] = [ { id: "sample", label: t.common.sample, icon: TestTube2, onClick: handleSample }, { id: "reset", label: t.common.reset, icon: Eraser, onClick: handleReset }, - { id: "copy", label: t.common.copy, icon: Copy, onClick: () => void handleCopy() }, + { id: "copy", label: t.common.copy, icon: Copy, onClick: handleCopy }, { id: "download", label: t.common.download, icon: Download, onClick: handleDownload }, ] diff --git a/src/features/tools/css-triangle-generator/page.tsx b/src/features/tools/css-triangle-generator/page.tsx index 7f4f9e4b..5dac22e2 100644 --- a/src/features/tools/css-triangle-generator/page.tsx +++ b/src/features/tools/css-triangle-generator/page.tsx @@ -68,7 +68,7 @@ export function CssTriangleGeneratorPage() { const actions: ToolAction[] = [ { id: "sample", label: t.common.sample, icon: TestTube2, onClick: handleSample }, { id: "reset", label: t.common.reset, icon: Eraser, onClick: handleReset }, - { id: "copy", label: t.common.copy, icon: Copy, onClick: () => void handleCopy() }, + { id: "copy", label: t.common.copy, icon: Copy, onClick: handleCopy }, { id: "download", label: t.common.download, icon: Download, onClick: handleDownload }, ] diff --git a/src/features/tools/devops-yaml-validator/page.tsx b/src/features/tools/devops-yaml-validator/page.tsx index 7c19743b..0ccdd8fc 100644 --- a/src/features/tools/devops-yaml-validator/page.tsx +++ b/src/features/tools/devops-yaml-validator/page.tsx @@ -34,7 +34,7 @@ export function DevopsYamlValidatorPage() { const actions: ToolAction[] = [ { id: "run", label: toolT.validate_action, icon: Play, onClick: run, variant: "default", disabled: !input.trim() }, - { id: "copy", label: t.common.copy, icon: Copy, onClick: () => void copyOutput(), disabled: !output }, + { id: "copy", label: t.common.copy, icon: Copy, onClick: copyOutput, disabled: !output }, { id: "sample", label: t.common.sample, icon: RotateCcw, onClick: () => { setInput(SAMPLE_INPUT); setOutput("") } }, { id: "clear", label: t.common.clear, icon: Eraser, onClick: () => { setInput(""); setOutput("") } }, ] diff --git a/src/features/tools/fake-iban-generator/page.tsx b/src/features/tools/fake-iban-generator/page.tsx index a425e57a..8750ebd0 100644 --- a/src/features/tools/fake-iban-generator/page.tsx +++ b/src/features/tools/fake-iban-generator/page.tsx @@ -143,7 +143,7 @@ export function FakeIbanGeneratorPage() { { id: "sample", label: t.common.sample, icon: TestTube2, onClick: handleSample }, { id: "generate", label: toolT.generate_action, icon: Play, onClick: handleGenerate }, { id: "reset", label: t.common.reset, icon: Eraser, onClick: handleReset }, - { id: "copy", label: t.common.copy, icon: Copy, onClick: () => void handleCopy() }, + { id: "copy", label: t.common.copy, icon: Copy, onClick: handleCopy }, { id: "download", label: t.common.download, icon: Download, onClick: handleDownload, disabled: items.length === 0 }, ] diff --git a/src/features/tools/google-fonts-pair-finder/page.tsx b/src/features/tools/google-fonts-pair-finder/page.tsx index 4d14105f..63a63751 100644 --- a/src/features/tools/google-fonts-pair-finder/page.tsx +++ b/src/features/tools/google-fonts-pair-finder/page.tsx @@ -146,7 +146,7 @@ export function GoogleFontsPairFinderPage() { const actions: ToolAction[] = [ { id: "shuffle", label: toolT.shuffle_action, icon: RefreshCw, onClick: handleShuffle, disabled: filteredPairs.length === 0 }, - { id: "copy_css", label: toolT.copy_css_action, icon: Copy, onClick: () => void handleCopyCss() }, + { id: "copy_css", label: toolT.copy_css_action, icon: Copy, onClick: handleCopyCss }, ] return ( diff --git a/src/features/tools/graphql-workbench/page.tsx b/src/features/tools/graphql-workbench/page.tsx index b638a81e..157f288e 100644 --- a/src/features/tools/graphql-workbench/page.tsx +++ b/src/features/tools/graphql-workbench/page.tsx @@ -60,7 +60,7 @@ export function GraphqlWorkbenchPage() { const actions: ToolAction[] = [ { id: "run", label: toolT.format_action, icon: Play, onClick: run, variant: "default", disabled: !query.trim() }, - { id: "copy", label: t.common.copy, icon: Copy, onClick: () => void copyOutput(), disabled: !output }, + { id: "copy", label: t.common.copy, icon: Copy, onClick: copyOutput, disabled: !output }, { id: "sample", label: t.common.sample, icon: RotateCcw, onClick: loadSample }, { id: "clear", label: t.common.clear, icon: Eraser, onClick: clear }, ] diff --git a/src/features/tools/http-request-builder/page.tsx b/src/features/tools/http-request-builder/page.tsx index b2987f42..a9e71720 100644 --- a/src/features/tools/http-request-builder/page.tsx +++ b/src/features/tools/http-request-builder/page.tsx @@ -131,7 +131,7 @@ export function HttpRequestBuilderPage() { id: "copy_code", label: t.common.copy, icon: Copy, - onClick: () => void handleCopy(), + onClick: handleCopy, disabled: !urlValidation.ok, disabledReason: urlValidation.ok ? undefined : urlValidationMessage, }, diff --git a/src/features/tools/image-average-color-finder/page.tsx b/src/features/tools/image-average-color-finder/page.tsx index 32413b23..b20c8aa4 100644 --- a/src/features/tools/image-average-color-finder/page.tsx +++ b/src/features/tools/image-average-color-finder/page.tsx @@ -119,7 +119,7 @@ export function ImageAverageColorFinderPage() { const actions: ToolAction[] = [ { id: "sample", label: t.common.sample, icon: TestTube2, onClick: handleSample }, { id: "reset", label: t.common.reset, icon: Eraser, onClick: handleReset }, - { id: "copy", label: t.common.copy, icon: Copy, onClick: () => void handleCopy() }, + { id: "copy", label: t.common.copy, icon: Copy, onClick: handleCopy }, { id: "download", label: t.common.download, icon: Download, onClick: handleDownload }, ] diff --git a/src/features/tools/image-caption-generator/page.tsx b/src/features/tools/image-caption-generator/page.tsx index 1c1c6ff1..a651da50 100644 --- a/src/features/tools/image-caption-generator/page.tsx +++ b/src/features/tools/image-caption-generator/page.tsx @@ -245,7 +245,7 @@ export function ImageCaptionGeneratorPage() { const actions: ToolAction[] = [ { id: "sample", label: t.common.sample, icon: TestTube2, onClick: handleSample }, { id: "reset", label: t.common.reset, icon: Eraser, onClick: handleReset }, - { id: "copy", label: t.common.copy, icon: Copy, onClick: () => void handleCopy() }, + { id: "copy", label: t.common.copy, icon: Copy, onClick: handleCopy }, { id: "download", label: t.common.download, icon: Download, onClick: handleDownload }, ] diff --git a/src/features/tools/image-color-extractor/page.tsx b/src/features/tools/image-color-extractor/page.tsx index 6c4c1c5d..5478c604 100644 --- a/src/features/tools/image-color-extractor/page.tsx +++ b/src/features/tools/image-color-extractor/page.tsx @@ -165,7 +165,7 @@ export function ImageColorExtractorPage() { const actions: ToolAction[] = [ { id: "sample", label: t.common.sample, icon: TestTube2, onClick: handleSample }, { id: "reset", label: t.common.reset, icon: Eraser, onClick: handleReset }, - { id: "copy", label: t.common.copy, icon: Copy, onClick: () => void handleCopy() }, + { id: "copy", label: t.common.copy, icon: Copy, onClick: handleCopy }, { id: "download", label: t.common.download, icon: Download, onClick: handleDownload }, ] diff --git a/src/features/tools/image-color-picker/page.tsx b/src/features/tools/image-color-picker/page.tsx index bb424781..ad871332 100644 --- a/src/features/tools/image-color-picker/page.tsx +++ b/src/features/tools/image-color-picker/page.tsx @@ -198,7 +198,7 @@ export function ImageColorPickerPage() { const actions: ToolAction[] = [ { id: "sample", label: t.common.sample, icon: TestTube2, onClick: handleSample }, { id: "reset", label: t.common.reset, icon: Eraser, onClick: handleReset }, - { id: "copy", label: t.common.copy, icon: Copy, onClick: () => void handleCopy() }, + { id: "copy", label: t.common.copy, icon: Copy, onClick: handleCopy }, { id: "download", label: t.common.download, icon: Download, onClick: handleDownload }, ] diff --git a/src/features/tools/image-cropper/page.tsx b/src/features/tools/image-cropper/page.tsx index 4530b8a6..0395894e 100644 --- a/src/features/tools/image-cropper/page.tsx +++ b/src/features/tools/image-cropper/page.tsx @@ -218,7 +218,7 @@ export function ImageCropperPage() { const actions: ToolAction[] = [ { id: "sample", label: t.common.sample, icon: TestTube2, onClick: handleSample }, { id: "reset", label: t.common.reset, icon: Eraser, onClick: handleReset }, - { id: "copy", label: t.common.copy, icon: Copy, onClick: () => void handleCopy(), disabled: isProcessing }, + { id: "copy", label: t.common.copy, icon: Copy, onClick: handleCopy, disabled: isProcessing }, { id: "download", label: t.common.download, icon: Download, onClick: handleDownload, disabled: isProcessing || !outputDataUrl }, ] diff --git a/src/features/tools/image-filters/page.tsx b/src/features/tools/image-filters/page.tsx index 5948238c..f1b5e122 100644 --- a/src/features/tools/image-filters/page.tsx +++ b/src/features/tools/image-filters/page.tsx @@ -189,7 +189,7 @@ export function ImageFiltersPage() { const actions: ToolAction[] = [ { id: "sample", label: t.common.sample, icon: TestTube2, onClick: handleSample }, { id: "reset", label: t.common.reset, icon: Eraser, onClick: handleReset }, - { id: "copy", label: t.common.copy, icon: Copy, onClick: () => void handleCopy(), disabled: isProcessing }, + { id: "copy", label: t.common.copy, icon: Copy, onClick: handleCopy, disabled: isProcessing }, { id: "download", label: t.common.download, icon: Download, onClick: handleDownload, disabled: isProcessing || !outputDataUrl }, ] diff --git a/src/features/tools/image-resizer/page.tsx b/src/features/tools/image-resizer/page.tsx index 4a9204fa..d4dbb504 100644 --- a/src/features/tools/image-resizer/page.tsx +++ b/src/features/tools/image-resizer/page.tsx @@ -283,7 +283,7 @@ export function ImageResizerPage() { } const actions: ToolAction[] = [ - { id: "sample", label: t.common.sample, icon: TestTube2, onClick: () => void handleSample() }, + { id: "sample", label: t.common.sample, icon: TestTube2, onClick: handleSample }, { id: "clear", label: t.common.clear, icon: Eraser, onClick: handleClear, destructive: true }, { id: "reset", label: t.common.reset, icon: RotateCcw, onClick: handleReset, destructive: true }, { diff --git a/src/features/tools/instagram-filters/page.tsx b/src/features/tools/instagram-filters/page.tsx index baa0d5c7..eaacceac 100644 --- a/src/features/tools/instagram-filters/page.tsx +++ b/src/features/tools/instagram-filters/page.tsx @@ -172,7 +172,7 @@ export function InstagramFiltersPage() { const actions: ToolAction[] = [ { id: "sample", label: t.common.sample, icon: TestTube2, onClick: handleSample }, { id: "reset", label: t.common.reset, icon: Eraser, onClick: handleReset }, - { id: "copy", label: t.common.copy, icon: Copy, onClick: () => void handleCopy() }, + { id: "copy", label: t.common.copy, icon: Copy, onClick: handleCopy }, { id: "download", label: t.common.download, icon: Download, onClick: handleDownload }, ] diff --git a/src/features/tools/instagram-post-generator/page.tsx b/src/features/tools/instagram-post-generator/page.tsx index d57af43c..96362024 100644 --- a/src/features/tools/instagram-post-generator/page.tsx +++ b/src/features/tools/instagram-post-generator/page.tsx @@ -254,7 +254,7 @@ export function InstagramPostGeneratorPage() { const actions: ToolAction[] = [ { id: "sample", label: t.common.sample, icon: TestTube2, onClick: handleSample }, { id: "reset", label: t.common.reset, icon: Eraser, onClick: handleReset }, - { id: "copy", label: t.common.copy, icon: Copy, onClick: () => void handleCopy() }, + { id: "copy", label: t.common.copy, icon: Copy, onClick: handleCopy }, { id: "download", label: t.common.download, icon: Download, onClick: handleDownload }, ] diff --git a/src/features/tools/instagram-story-generator/page.tsx b/src/features/tools/instagram-story-generator/page.tsx index 438d66e2..04b708e6 100644 --- a/src/features/tools/instagram-story-generator/page.tsx +++ b/src/features/tools/instagram-story-generator/page.tsx @@ -281,7 +281,7 @@ export function InstagramStoryGeneratorPage() { const actions: ToolAction[] = [ { id: "sample", label: t.common.sample, icon: TestTube2, onClick: handleSample }, { id: "reset", label: t.common.reset, icon: Eraser, onClick: handleReset }, - { id: "copy", label: t.common.copy, icon: Copy, onClick: () => void handleCopy() }, + { id: "copy", label: t.common.copy, icon: Copy, onClick: handleCopy }, { id: "download", label: t.common.download, icon: Download, onClick: handleDownload }, ] diff --git a/src/features/tools/json-schema-workbench/page.tsx b/src/features/tools/json-schema-workbench/page.tsx index be02125c..c464d4d7 100644 --- a/src/features/tools/json-schema-workbench/page.tsx +++ b/src/features/tools/json-schema-workbench/page.tsx @@ -62,7 +62,7 @@ export function JsonSchemaWorkbenchPage() { const actions: ToolAction[] = [ { id: "run", label: mode === "generate" ? toolT.generate_action : toolT.validate_action, icon: Play, onClick: run, variant: "default", disabled: !payload.trim() }, - { id: "copy", label: t.common.copy, icon: Copy, onClick: () => void copyOutput(), disabled: !output }, + { id: "copy", label: t.common.copy, icon: Copy, onClick: copyOutput, disabled: !output }, { id: "sample", label: t.common.sample, icon: RotateCcw, onClick: loadSample }, { id: "clear", label: t.common.clear, icon: Eraser, onClick: clear }, ] diff --git a/src/features/tools/jsonpath-playground/page.tsx b/src/features/tools/jsonpath-playground/page.tsx index ec5863e3..389bff1c 100644 --- a/src/features/tools/jsonpath-playground/page.tsx +++ b/src/features/tools/jsonpath-playground/page.tsx @@ -88,7 +88,7 @@ export function JsonPathPlaygroundPage() { const actions: ToolAction[] = [ { id: "sample", label: t.common.sample, icon: TestTube2, onClick: handleSample }, - { id: "copy", label: t.common.copy, icon: Copy, onClick: () => void handleCopyResult(), disabled: !result }, + { id: "copy", label: t.common.copy, icon: Copy, onClick: handleCopyResult, disabled: !result }, { id: "clear", label: t.common.clear, icon: Trash2, onClick: () => { setJson(""); setPath(""); } }, ] diff --git a/src/features/tools/jwt-verifier/page.tsx b/src/features/tools/jwt-verifier/page.tsx index 5af72d46..4028f942 100644 --- a/src/features/tools/jwt-verifier/page.tsx +++ b/src/features/tools/jwt-verifier/page.tsx @@ -6,7 +6,7 @@ import Link from "next/link" import { Textarea } from "@/components/ui/textarea" import { useLang } from "@/core/i18n/lang-provider" import { RelatedTools } from "@/core/seo/components/related-tools" -import { ToolActionBar, type ToolAction } from "@/features/tool-shell/tool-action-bar" +import { ToolActionBar, type ToolAction, type ToolActionResult } from "@/features/tool-shell/tool-action-bar" import { SensitiveInputWarning } from "@/features/tool-shell/sensitive-input-warning" import { JwtSecretField } from "@/features/tools/jwt-workbench/jwt-secret-field" import { safeClipboardWrite } from "@/core/clipboard/clipboard" @@ -63,19 +63,50 @@ export function JwtVerifierPage() { const [payload, setPayload] = React.useState | null>(null) const [claims, setClaims] = React.useState<{ label: string; status: string; value: string }[]>([]) - const verify = async () => { - if (!token.trim()) return { status: "failed" as const, message: toolT.token_label } + const verify = async (): Promise => { + if (!token.trim()) return { status: "failed", message: t.common.input_required, description: toolT.token_label } const h = decodeHeader(token) const p = decodePayload(token) setHeader(h) setPayload(p) const alg = normalizeJwtAlgorithm(h?.alg) + const algorithmClass = alg.toLowerCase() === "none" + ? "unsigned" + : alg === "HS256" || alg === "HS384" || alg === "HS512" + ? "hmac" + : "unsupported" if (p) setClaims(checkClaims(p, claimLabels)) - setVerifyResult(await verifyJwtSignature(token, secret, alg)) - return { status: "success" as const } + if (algorithmClass === "hmac" && !secret.trim()) { + setVerifyResult(null) + return { + status: "failed", + message: t.common.input_required, + description: toolT.secret_label, + } + } + + const nextVerifyResult = await verifyJwtSignature(token, secret, alg) + setVerifyResult(nextVerifyResult) + if (!nextVerifyResult) { + return { + status: "failed", + message: t.common.input_required, + description: toolT.secret_label, + } + } + if (nextVerifyResult.status === "valid") { + return { status: "success", message: toolT.result_valid.replace("{alg}", nextVerifyResult.algorithm) } + } + if (nextVerifyResult.status === "invalid") { + return { status: "failed", message: toolT.result_invalid.replace("{alg}", nextVerifyResult.algorithm) } + } + if (nextVerifyResult.status === "unsigned") { + return { status: "success", message: toolT.result_none_warning } + } + return { status: "success", message: toolT.result_unsupported.replace("{alg}", nextVerifyResult.algorithm) } } const clearAll = () => { setToken("") diff --git a/src/features/tools/jwt-workbench/page.tsx b/src/features/tools/jwt-workbench/page.tsx index 9eacb88b..739dda4d 100644 --- a/src/features/tools/jwt-workbench/page.tsx +++ b/src/features/tools/jwt-workbench/page.tsx @@ -252,7 +252,7 @@ export function JwtWorkbenchPage() { id: "encode", label: toolT.encode_action, icon: ShieldCheck, - onClick: () => { void encodeToken() }, + onClick: encodeToken, variant: "default", }, ] diff --git a/src/features/tools/log-scrubber/page.tsx b/src/features/tools/log-scrubber/page.tsx index 996f80da..283139fe 100644 --- a/src/features/tools/log-scrubber/page.tsx +++ b/src/features/tools/log-scrubber/page.tsx @@ -122,7 +122,7 @@ password=hunter2`) id: "copy", label: t.common.copy, icon: Copy, - onClick: () => void copyOutput(), + onClick: copyOutput, disabled: !output, disabledReason: t.common.action_disabled_no_output, }, diff --git a/src/features/tools/messagepack-inspector/page.tsx b/src/features/tools/messagepack-inspector/page.tsx index dbd5ec77..fbc6e349 100644 --- a/src/features/tools/messagepack-inspector/page.tsx +++ b/src/features/tools/messagepack-inspector/page.tsx @@ -47,7 +47,7 @@ export function MessagepackInspectorPage() { const actions: ToolAction[] = [ { id: "run", label: toolT.decode_action, icon: Play, onClick: run, variant: "default", disabled: !input.trim() }, - { id: "copy", label: t.common.copy, icon: Copy, onClick: () => void copyOutput(), disabled: !output }, + { id: "copy", label: t.common.copy, icon: Copy, onClick: copyOutput, disabled: !output }, { id: "sample", label: t.common.sample, icon: RotateCcw, onClick: () => { setInput(SAMPLE_INPUT); setOutput(""); setSummary(""); setError(null) } }, { id: "clear", label: t.common.clear, icon: Eraser, onClick: () => { setInput(""); setOutput(""); setSummary(""); setError(null) } }, ] diff --git a/src/features/tools/oauth-jwks-workbench/page.tsx b/src/features/tools/oauth-jwks-workbench/page.tsx index 21b16fec..e25958ea 100644 --- a/src/features/tools/oauth-jwks-workbench/page.tsx +++ b/src/features/tools/oauth-jwks-workbench/page.tsx @@ -2,13 +2,12 @@ import * as React from "react" import { Copy, Eraser, KeyRound, Play, RotateCcw, ShieldCheck } from "lucide-react" -import { toast } from "sonner" import { Textarea } from "@/components/ui/textarea" import { useLang } from "@/core/i18n/lang-provider" -import { safeClipboardWrite } from "@/core/clipboard/clipboard" import { RelatedTools } from "@/core/seo/components/related-tools" import { SensitiveInputWarning } from "@/features/tool-shell/sensitive-input-warning" import { ToolActionBar, type ToolAction } from "@/features/tool-shell/tool-action-bar" +import { copyTextWithToolFeedback, notifyToolActionFailure } from "@/features/tool-shell/tool-action-feedback" import { generatePkcePair, summarizeJwks, verifyJwtWithJwks } from "./logic" import { SAMPLE_INPUT, SAMPLE_JWT } from "./samples" @@ -80,15 +79,13 @@ export function OauthJwksWorkbenchPage() { const copyOutput = async () => { if (!output) { - return { status: "failed" as const } - } - const result = await safeClipboardWrite(output) - if (!result.ok) { - toast.error(t.common.copy_failed) - return { status: "failed" as const, message: t.common.copy_failed } + return notifyToolActionFailure(t, { + kind: "copy", + label: t.common.output, + title: t.common.action_disabled_no_output, + }) } - toast.success(t.common.copied) - return { status: "success" as const, message: t.common.copied } + return copyTextWithToolFeedback(t, output, t.common.output) } /* diff --git a/src/features/tools/openapi-diff/page.tsx b/src/features/tools/openapi-diff/page.tsx index 66cb6b8b..66066a34 100644 --- a/src/features/tools/openapi-diff/page.tsx +++ b/src/features/tools/openapi-diff/page.tsx @@ -57,7 +57,7 @@ export function OpenapiDiffPage() { const actions: ToolAction[] = [ { id: "run", label: toolT.compare_action, icon: Play, onClick: run, variant: "default", disabled: !before.trim() || !after.trim() }, - { id: "copy", label: t.common.copy, icon: Copy, onClick: () => void copyOutput(), disabled: !output }, + { id: "copy", label: t.common.copy, icon: Copy, onClick: copyOutput, disabled: !output }, { id: "download", label: t.common.download, icon: Download, onClick: () => downloadText("openapi-diff-report.txt", output), disabled: !output }, { id: "sample", label: t.common.sample, icon: RotateCcw, onClick: loadSample }, { id: "clear", label: t.common.clear, icon: Eraser, onClick: clear }, diff --git a/src/features/tools/openapi-viewer/page.tsx b/src/features/tools/openapi-viewer/page.tsx index 24f299cf..16bbe263 100644 --- a/src/features/tools/openapi-viewer/page.tsx +++ b/src/features/tools/openapi-viewer/page.tsx @@ -166,7 +166,7 @@ export function OpenApiViewerPage() { id: "copy", label: t.common.copy, icon: Copy, - onClick: () => void handleCopyInput(), + onClick: handleCopyInput, disabled: !input, }, { diff --git a/src/features/tools/password-generator/page.tsx b/src/features/tools/password-generator/page.tsx index c4febbb7..8bb80486 100644 --- a/src/features/tools/password-generator/page.tsx +++ b/src/features/tools/password-generator/page.tsx @@ -192,7 +192,7 @@ export function PasswordGeneratorPage() { id: "copyAll", label: text("copy_all"), icon: Copy, - onClick: () => void handleCopyAll(), + onClick: handleCopyAll, disabled: !results.length, variant: "default", }, diff --git a/src/features/tools/photo-censor/page.tsx b/src/features/tools/photo-censor/page.tsx index 0a31fd16..9811a8f5 100644 --- a/src/features/tools/photo-censor/page.tsx +++ b/src/features/tools/photo-censor/page.tsx @@ -220,7 +220,7 @@ export function PhotoCensorPage() { const actions: ToolAction[] = [ { id: "sample", label: t.common.sample, icon: TestTube2, onClick: handleSample }, { id: "reset", label: t.common.reset, icon: Eraser, onClick: handleReset }, - { id: "copy", label: t.common.copy, icon: Copy, onClick: () => void handleCopy(), disabled: isProcessing }, + { id: "copy", label: t.common.copy, icon: Copy, onClick: handleCopy, disabled: isProcessing }, { id: "download", label: t.common.download, icon: Download, onClick: handleDownload, disabled: isProcessing || !outputDataUrl }, ] diff --git a/src/features/tools/pipeline-builder/page.tsx b/src/features/tools/pipeline-builder/page.tsx index 071833e6..d3422844 100644 --- a/src/features/tools/pipeline-builder/page.tsx +++ b/src/features/tools/pipeline-builder/page.tsx @@ -355,7 +355,7 @@ export function PipelineBuilderPage() { const actions: ToolAction[] = [ { id: "sample", label: t.common.sample, icon: FileInput, onClick: loadSample }, - { id: "run", label: isRunning ? text("running") : text("run_recipe"), icon: Play, onClick: () => void runCurrentRecipe(), disabled: isRunning || recipe.steps.length === 0 }, + { id: "run", label: isRunning ? text("running") : text("run_recipe"), icon: Play, onClick: runCurrentRecipe, disabled: isRunning || recipe.steps.length === 0 }, { id: "save", label: text("save_recipe"), icon: Save, onClick: () => requestPrivacyPreview("save"), disabled: !storageAvailable }, { id: "export", label: text("export_recipe"), icon: Download, onClick: () => requestPrivacyPreview("export") }, { id: "share", label: text("share_recipe"), icon: Link2, onClick: () => requestPrivacyPreview("share") }, diff --git a/src/features/tools/public-key-jwk-helper/page.tsx b/src/features/tools/public-key-jwk-helper/page.tsx index 23678f40..664e6ba7 100644 --- a/src/features/tools/public-key-jwk-helper/page.tsx +++ b/src/features/tools/public-key-jwk-helper/page.tsx @@ -113,7 +113,7 @@ export function PublicKeyJwkHelperPage() { id: "copy_output", label: t.common.copy, icon: Copy, - onClick: () => void copyOutput(), + onClick: copyOutput, disabled: !result?.output, }, { diff --git a/src/features/tools/qr-code-generator/page.tsx b/src/features/tools/qr-code-generator/page.tsx index b3b6a8e6..8048d8a3 100644 --- a/src/features/tools/qr-code-generator/page.tsx +++ b/src/features/tools/qr-code-generator/page.tsx @@ -249,14 +249,14 @@ export function QrCodeGeneratorPage() { id: "svg", label: "SVG", icon: Download, - onClick: () => void downloadSvg(), + onClick: downloadSvg, disabled: !text.trim(), }, { id: "copy", label: t.common.copy, icon: Copy, - onClick: () => void handleCopyDataUrl(), + onClick: handleCopyDataUrl, disabled: !dataUrl, }, { diff --git a/src/features/tools/react-native-shadow-generator/page.tsx b/src/features/tools/react-native-shadow-generator/page.tsx index 554f4133..24e14dd8 100644 --- a/src/features/tools/react-native-shadow-generator/page.tsx +++ b/src/features/tools/react-native-shadow-generator/page.tsx @@ -101,7 +101,7 @@ export function ReactNativeShadowGeneratorPage() { const actions: ToolAction[] = [ { id: "sample", label: t.common.sample, icon: TestTube2, onClick: handleSample }, { id: "reset", label: t.common.reset, icon: Eraser, onClick: handleReset }, - { id: "copy", label: t.common.copy, icon: Copy, onClick: () => void handleCopy() }, + { id: "copy", label: t.common.copy, icon: Copy, onClick: handleCopy }, ] return ( diff --git a/src/features/tools/scanned-pdf-converter/page.tsx b/src/features/tools/scanned-pdf-converter/page.tsx index e862b4d0..5187fc3d 100644 --- a/src/features/tools/scanned-pdf-converter/page.tsx +++ b/src/features/tools/scanned-pdf-converter/page.tsx @@ -298,10 +298,10 @@ export function ScannedPdfConverterPage() { } const actions: ToolAction[] = [ - { id: "sample", label: t.common.sample, icon: TestTube2, onClick: () => void handleSample() }, + { id: "sample", label: t.common.sample, icon: TestTube2, onClick: handleSample }, { id: "reset", label: t.common.reset, icon: Eraser, onClick: handleReset }, - { id: "copy", label: t.common.copy, icon: Copy, onClick: () => void handleCopy() }, - { id: "download", label: isBusy ? toolT.exporting : t.common.download, icon: Download, onClick: () => void handleDownload(), disabled: pages.length === 0 || isBusy }, + { id: "copy", label: t.common.copy, icon: Copy, onClick: handleCopy }, + { id: "download", label: isBusy ? toolT.exporting : t.common.download, icon: Download, onClick: handleDownload, disabled: pages.length === 0 || isBusy }, ] return ( diff --git a/src/features/tools/security-header-analyzer/page.tsx b/src/features/tools/security-header-analyzer/page.tsx index d955b89b..45085f69 100644 --- a/src/features/tools/security-header-analyzer/page.tsx +++ b/src/features/tools/security-header-analyzer/page.tsx @@ -94,9 +94,7 @@ export function SecurityHeaderAnalyzerPage() { id: "copy_output", label: t.common.copy, icon: Copy, - onClick: () => { - void handleCopyReport() - }, + onClick: handleCopyReport, }, ] diff --git a/src/features/tools/seo-metadata-workbench/page.tsx b/src/features/tools/seo-metadata-workbench/page.tsx index 832e8c8d..84a6ba97 100644 --- a/src/features/tools/seo-metadata-workbench/page.tsx +++ b/src/features/tools/seo-metadata-workbench/page.tsx @@ -42,7 +42,7 @@ export function SeoMetadataWorkbenchPage() { const actions: ToolAction[] = [ { id: "run", label: toolT.analyze_action, icon: Play, onClick: run, variant: "default", disabled: !input.trim() }, - { id: "copy", label: t.common.copy, icon: Copy, onClick: () => void copyOutput(), disabled: !output }, + { id: "copy", label: t.common.copy, icon: Copy, onClick: copyOutput, disabled: !output }, { id: "download", label: t.common.download, icon: Download, onClick: () => downloadText("seo-metadata-report.txt", output), disabled: !output }, { id: "sample", label: t.common.sample, icon: RotateCcw, onClick: () => { setInput(SAMPLE_INPUT); setOutput(""); setError(null) } }, { id: "clear", label: t.common.clear, icon: Eraser, onClick: () => { setInput(""); setOutput(""); setError(null) } }, diff --git a/src/features/tools/slugify-case-converter/page.tsx b/src/features/tools/slugify-case-converter/page.tsx index bfbcbe08..f78892b4 100644 --- a/src/features/tools/slugify-case-converter/page.tsx +++ b/src/features/tools/slugify-case-converter/page.tsx @@ -104,7 +104,7 @@ export function SlugifyCaseConverterPage() { id: "copy_all", label: toolT.copy_all, icon: Copy, - onClick: () => void handleCopyAll(), + onClick: handleCopyAll, disabled: !input.trim(), }, ] diff --git a/src/features/tools/svg-blob-generator/page.tsx b/src/features/tools/svg-blob-generator/page.tsx index bc256700..4eb508ef 100644 --- a/src/features/tools/svg-blob-generator/page.tsx +++ b/src/features/tools/svg-blob-generator/page.tsx @@ -96,7 +96,7 @@ export function SvgBlobGeneratorPage() { { id: "sample", label: t.common.sample, icon: TestTube2, onClick: handleSample }, { id: "random", label: toolT.random_action, icon: Sparkles, onClick: handleRandomize }, { id: "reset", label: t.common.reset, icon: Eraser, onClick: handleReset }, - { id: "copy", label: t.common.copy, icon: Copy, onClick: () => void handleCopy() }, + { id: "copy", label: t.common.copy, icon: Copy, onClick: handleCopy }, { id: "download", label: t.common.download, icon: Download, onClick: handleDownload }, ] diff --git a/src/features/tools/svg-pattern-generator/page.tsx b/src/features/tools/svg-pattern-generator/page.tsx index 4e666b4f..68f3d416 100644 --- a/src/features/tools/svg-pattern-generator/page.tsx +++ b/src/features/tools/svg-pattern-generator/page.tsx @@ -102,7 +102,7 @@ export function SvgPatternGeneratorPage() { { id: "sample", label: t.common.sample, icon: TestTube2, onClick: handleSample }, { id: "random", label: toolT.randomize, icon: Sparkles, onClick: handleRandomize }, { id: "reset", label: t.common.reset, icon: Eraser, onClick: handleReset }, - { id: "copy", label: t.common.copy, icon: Copy, onClick: () => void handleCopy() }, + { id: "copy", label: t.common.copy, icon: Copy, onClick: handleCopy }, { id: "download", label: t.common.download, icon: Download, onClick: handleDownload }, ] diff --git a/src/features/tools/svg-stroke-to-fill-converter/page.tsx b/src/features/tools/svg-stroke-to-fill-converter/page.tsx index b1765aca..02aedc72 100644 --- a/src/features/tools/svg-stroke-to-fill-converter/page.tsx +++ b/src/features/tools/svg-stroke-to-fill-converter/page.tsx @@ -78,7 +78,7 @@ export function SvgStrokeToFillConverterPage() { const actions: ToolAction[] = [ { id: "sample", label: t.common.sample, icon: TestTube2, onClick: handleSample }, { id: "reset", label: t.common.reset, icon: Eraser, onClick: handleReset }, - { id: "copy", label: t.common.copy, icon: Copy, onClick: () => void handleCopy(), disabled: !output }, + { id: "copy", label: t.common.copy, icon: Copy, onClick: handleCopy, disabled: !output }, { id: "download", label: t.common.download, icon: Download, onClick: handleDownload, disabled: !output || Boolean(result.error) }, ] diff --git a/src/features/tools/svg-to-png-converter/page.tsx b/src/features/tools/svg-to-png-converter/page.tsx index 458dfbd9..70f7cdc0 100644 --- a/src/features/tools/svg-to-png-converter/page.tsx +++ b/src/features/tools/svg-to-png-converter/page.tsx @@ -208,7 +208,7 @@ export function SvgToPngConverterPage() { const actions: ToolAction[] = [ { id: "sample", label: t.common.sample, icon: TestTube2, onClick: handleSample }, { id: "reset", label: t.common.reset, icon: Eraser, onClick: handleReset }, - { id: "copy", label: t.common.copy, icon: Copy, onClick: () => void handleCopy() }, + { id: "copy", label: t.common.copy, icon: Copy, onClick: handleCopy }, { id: "download", label: t.common.download, icon: Download, onClick: handleDownload, disabled: !previewPng }, ] diff --git a/src/features/tools/text-to-handwriting-converter/page.tsx b/src/features/tools/text-to-handwriting-converter/page.tsx index 6c544fa9..1e0b3f6d 100644 --- a/src/features/tools/text-to-handwriting-converter/page.tsx +++ b/src/features/tools/text-to-handwriting-converter/page.tsx @@ -223,7 +223,7 @@ export function TextToHandwritingConverterPage() { { id: "sample", label: t.common.sample, icon: TestTube2, onClick: handleSample }, { id: "rerender", label: toolT.render_action, icon: RefreshCw, onClick: () => setInput((value) => value) }, { id: "reset", label: t.common.reset, icon: Eraser, onClick: handleReset }, - { id: "copy", label: t.common.copy, icon: Copy, onClick: () => void handleCopy() }, + { id: "copy", label: t.common.copy, icon: Copy, onClick: handleCopy }, { id: "download", label: toolT.download_png_action, icon: Download, onClick: handleDownload, disabled: !dataUrl }, ] diff --git a/src/features/tools/tweet-generator/page.tsx b/src/features/tools/tweet-generator/page.tsx index 6b345cf5..842bbd1c 100644 --- a/src/features/tools/tweet-generator/page.tsx +++ b/src/features/tools/tweet-generator/page.tsx @@ -298,7 +298,7 @@ export function TweetGeneratorPage() { const actions: ToolAction[] = [ { id: "sample", label: t.common.sample, icon: TestTube2, onClick: handleSample }, { id: "reset", label: t.common.reset, icon: Eraser, onClick: handleReset }, - { id: "copy", label: t.common.copy, icon: Copy, onClick: () => void handleCopy() }, + { id: "copy", label: t.common.copy, icon: Copy, onClick: handleCopy }, { id: "download", label: t.common.download, icon: Download, onClick: handleDownload }, ] diff --git a/src/features/tools/tweet-to-image-converter/page.tsx b/src/features/tools/tweet-to-image-converter/page.tsx index 9ae2fc2b..3b93a915 100644 --- a/src/features/tools/tweet-to-image-converter/page.tsx +++ b/src/features/tools/tweet-to-image-converter/page.tsx @@ -331,7 +331,7 @@ export function TweetToImageConverterPage() { const actions: ToolAction[] = [ { id: "sample", label: t.common.sample, icon: TestTube2, onClick: handleSample }, { id: "reset", label: t.common.reset, icon: Eraser, onClick: handleReset }, - { id: "copy", label: t.common.copy, icon: Copy, onClick: () => void handleCopy() }, + { id: "copy", label: t.common.copy, icon: Copy, onClick: handleCopy }, { id: "download", label: t.common.download, icon: Download, onClick: handleDownload }, ] diff --git a/src/features/tools/twitter-ad-revenue-generator/page.tsx b/src/features/tools/twitter-ad-revenue-generator/page.tsx index e21b4e29..27a42771 100644 --- a/src/features/tools/twitter-ad-revenue-generator/page.tsx +++ b/src/features/tools/twitter-ad-revenue-generator/page.tsx @@ -118,7 +118,7 @@ export function TwitterAdRevenueGeneratorPage() { const actions: ToolAction[] = [ { id: "sample", label: t.common.sample, icon: TestTube2, onClick: handleSample }, { id: "reset", label: t.common.reset, icon: Eraser, onClick: handleReset }, - { id: "copy", label: t.common.copy, icon: Copy, onClick: () => void handleCopy() }, + { id: "copy", label: t.common.copy, icon: Copy, onClick: handleCopy }, { id: "download", label: t.common.download, icon: Download, onClick: handleDownload }, ] diff --git a/src/features/tools/uuid-generator/page.tsx b/src/features/tools/uuid-generator/page.tsx index a9757f47..fdb38c4c 100644 --- a/src/features/tools/uuid-generator/page.tsx +++ b/src/features/tools/uuid-generator/page.tsx @@ -105,7 +105,7 @@ export function UuidGeneratorPage() { id: "copy_all", label: t.common.copy_all, icon: Copy, - onClick: () => void handleCopyAll(), + onClick: handleCopyAll, disabled: formattedUuids.length === 0, disabledReason: t.common.action_disabled_no_output, }, diff --git a/tests/component/jwt-verifier-page.test.tsx b/tests/component/jwt-verifier-page.test.tsx index 98809903..8d805384 100644 --- a/tests/component/jwt-verifier-page.test.tsx +++ b/tests/component/jwt-verifier-page.test.tsx @@ -71,19 +71,38 @@ describe("JwtVerifierPage", () => { fireEvent.click(screen.getByRole("button", { name: "Verify" })) await waitFor(() => { - expect(screen.getByText(/Signature matches the supplied secret \(HS256\)/)).toBeInTheDocument() - expect(screen.getByText(/not overall token trust/)).toBeInTheDocument() + expect(screen.getAllByText(/Signature matches the supplied secret \(HS256\)/).length).toBeGreaterThan(0) + expect(screen.getAllByText(/not overall token trust/).length).toBeGreaterThan(0) }) fireEvent.change(screen.getByLabelText("Secret Key (for HMAC verification)"), { target: { value: "wrong-secret" } }) fireEvent.click(screen.getByRole("button", { name: "Verify" })) await waitFor(() => { - expect(screen.getByText(/Signature does not match the supplied secret \(HS256\)/)).toBeInTheDocument() - expect(screen.getByText(/Do not trust this token for that key/)).toBeInTheDocument() + expect(screen.getAllByText(/Signature does not match the supplied secret \(HS256\)/).length).toBeGreaterThan(0) + expect(screen.getAllByText(/Do not trust this token for that key/).length).toBeGreaterThan(0) }) }) + it("does not announce verification success before required HMAC secret input is present", async () => { + renderJwtVerifier() + + const verifyButton = screen.getByRole("button", { name: "Verify" }) + expect(verifyButton).toBeDisabled() + expect(verifyButton).toHaveAttribute("title", "Verify: JWT Token") + + fireEvent.change(screen.getByRole("textbox", { name: "JWT Token" }), { target: { value: unsignedToken("HS256") } }) + expect(screen.getByRole("button", { name: "Verify" })).not.toBeDisabled() + + fireEvent.click(screen.getByRole("button", { name: "Verify" })) + + await waitFor(() => { + expect(screen.getByRole("status")).toHaveTextContent("Input is required. Secret Key (for HMAC verification)") + }) + expect(screen.getByRole("button", { name: "Verify" })).toHaveAttribute("data-tool-action-state", "failed") + expect(screen.queryByText(/Signature matches/)).not.toBeInTheDocument() + }) + it("shows unsupported algorithms without reporting an invalid signature", async () => { renderJwtVerifier() @@ -92,7 +111,7 @@ describe("JwtVerifierPage", () => { fireEvent.click(screen.getByRole("button", { name: "Verify" })) await waitFor(() => { - expect(screen.getByText(/Unsupported JWT algorithm \(RS256\)/)).toBeInTheDocument() + expect(screen.getAllByText(/Unsupported JWT algorithm \(RS256\)/).length).toBeGreaterThan(0) expect(screen.queryByText(/Signature does not match/)).not.toBeInTheDocument() }) }) @@ -104,7 +123,7 @@ describe("JwtVerifierPage", () => { fireEvent.click(screen.getByRole("button", { name: "Verify" })) await waitFor(() => { - expect(screen.getByText(/declares alg: none/)).toBeInTheDocument() + expect(screen.getAllByText(/declares alg: none/).length).toBeGreaterThan(0) expect(screen.queryByText(/Signature does not match/)).not.toBeInTheDocument() }) }) @@ -117,7 +136,7 @@ describe("JwtVerifierPage", () => { fireEvent.click(screen.getByRole("button", { name: "Verify" })) await waitFor(() => { - expect(screen.getByText(/Unsupported JWT algorithm \(non-string alg\)/)).toBeInTheDocument() + expect(screen.getAllByText(/Unsupported JWT algorithm \(non-string alg\)/).length).toBeGreaterThan(0) expect(screen.queryByText(/Signature does not match/)).not.toBeInTheDocument() }) }) diff --git a/tests/component/oauth-jwks-workbench-page.test.tsx b/tests/component/oauth-jwks-workbench-page.test.tsx index 62c9ab32..0f5832a5 100644 --- a/tests/component/oauth-jwks-workbench-page.test.tsx +++ b/tests/component/oauth-jwks-workbench-page.test.tsx @@ -1,9 +1,15 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/react" -import { describe, expect, it, vi } from "vitest" +import { beforeEach, describe, expect, it, vi } from "vitest" import { LangProvider } from "@/core/i18n/lang-provider" import { getTranslation } from "@/core/i18n/translations/catalog" import { OauthJwksWorkbenchPage } from "@/features/tools/oauth-jwks-workbench/page" +const clipboardWriteMock = vi.hoisted(() => vi.fn()) + +vi.mock("@/core/clipboard/clipboard", () => ({ + safeClipboardWrite: (value: string) => clipboardWriteMock(value), +})) + vi.mock("next/link", () => ({ default: ({ href, children, ...props }: { href: string; children: React.ReactNode }) => ( @@ -32,6 +38,10 @@ function renderOauthJwksWorkbench() { } describe("OauthJwksWorkbenchPage", () => { + beforeEach(() => { + clipboardWriteMock.mockReset() + }) + it("shows only the PKCE workflow by default and toggles JWKS inputs by mode", async () => { const originalCrypto = globalThis.crypto const subtleDigest = vi.fn().mockResolvedValue(new Uint8Array(32).buffer) @@ -96,4 +106,43 @@ describe("OauthJwksWorkbenchPage", () => { }) } }) + + it("returns clipboard failures through the shared toolbar status path", async () => { + const originalCrypto = globalThis.crypto + clipboardWriteMock.mockResolvedValue({ ok: false, method: "none" }) + + try { + Object.defineProperty(globalThis, "crypto", { + configurable: true, + value: { + getRandomValues: vi.fn((array: Uint8Array) => { + array.fill(1) + return array + }), + subtle: { + digest: vi.fn().mockResolvedValue(new Uint8Array(32).buffer), + }, + }, + }) + + renderOauthJwksWorkbench() + fireEvent.click(screen.getByRole("button", { name: "Generate PKCE" })) + + await waitFor(() => { + expect(screen.getByRole("textbox", { name: "Output" })).toHaveDisplayValue(/"verifier"/) + }) + + fireEvent.click(screen.getByRole("button", { name: "Copy" })) + + await waitFor(() => { + expect(screen.getByRole("status")).toHaveTextContent("Unable to copy. Please copy manually.") + }) + expect(screen.getByRole("button", { name: "Copy" })).toHaveAttribute("data-tool-action-state", "failed") + } finally { + Object.defineProperty(globalThis, "crypto", { + configurable: true, + value: originalCrypto, + }) + } + }) }) diff --git a/tests/component/tool-shell-status-output.test.tsx b/tests/component/tool-shell-status-output.test.tsx index 38400e61..0bc7713b 100644 --- a/tests/component/tool-shell-status-output.test.tsx +++ b/tests/component/tool-shell-status-output.test.tsx @@ -54,6 +54,25 @@ describe("shared tool shell status and output components", () => { }) }) + it("does not announce async Promise actions as successful without a returned result", async () => { + const actions: ToolAction[] = [ + { + id: "verify", + label: "Verify", + onClick: async () => undefined, + }, + ] + + renderEnglish() + + fireEvent.click(screen.getByRole("button", { name: "Verify" })) + + await waitFor(() => { + expect(screen.queryByRole("status")).not.toBeInTheDocument() + }) + expect(screen.getByRole("button", { name: "Verify" })).toHaveAttribute("data-tool-action-state", "idle") + }) + it("lets long output switch between wrap and horizontal scroll modes without changing the output text", () => { const output = `https://example.com/${"a".repeat(180)}` diff --git a/tests/guards/tool-action-consistency-guard.test.ts b/tests/guards/tool-action-consistency-guard.test.ts index c08f43a9..d370364c 100644 --- a/tests/guards/tool-action-consistency-guard.test.ts +++ b/tests/guards/tool-action-consistency-guard.test.ts @@ -1,11 +1,31 @@ -import { readFileSync } from "node:fs" +import { readdirSync, readFileSync, statSync } from "node:fs" +import { join } from "node:path" import { describe, expect, it } from "vitest" function read(path: string) { return readFileSync(path, "utf8") } +function listFiles(dir: string): string[] { + return readdirSync(dir).flatMap((entry) => { + const path = join(dir, entry) + return statSync(path).isDirectory() ? listFiles(path) : [path] + }) +} + describe("tool action consistency guard", () => { + it("prevents shared ToolAction handlers from hiding async work behind void wrappers", () => { + const files = listFiles("src/features") + .filter((file) => file.endsWith(".tsx")) + .filter((file) => read(file).includes("ToolAction")) + + for (const file of files) { + const source = read(file) + expect(source, file).not.toMatch(/onClick:\s*\(\)\s*=>\s*void\s+[A-Za-z_$][\w$]*\s*\(/) + expect(source, file).not.toMatch(/onClick:\s*\(\)\s*=>\s*\{\s*void\s+[A-Za-z_$][\w$]*\s*\(/) + } + }) + it("documents shared Sample, Clear, Reset, disabled, and destructive action semantics", () => { const designSystem = read("docs/specs/design-system.md")