diff --git a/hdyc-svelte/src/lib/components/Calculator.svelte b/hdyc-svelte/src/lib/components/Calculator.svelte index 8ee9e11..0326b97 100644 --- a/hdyc-svelte/src/lib/components/Calculator.svelte +++ b/hdyc-svelte/src/lib/components/Calculator.svelte @@ -2,7 +2,7 @@ import { solve } from '$lib/engine'; import type { CalculatorDef } from '$lib/data/calculators'; import { page } from '$app/stores'; - import { onMount } from 'svelte'; + import { onDestroy, onMount } from 'svelte'; import { browser } from '$app/environment'; import QuickDefinitionCard from '$lib/components/QuickDefinitionCard.svelte'; import QuickConversionExample from '$lib/components/QuickConversionExample.svelte'; @@ -18,8 +18,12 @@ let swapState: { originalField: 1 | 2; originalValue: string } | null = null; let copyStatus: 'idle' | 'copied' | 'failed' = 'idle'; let statusTimeout: ReturnType | null = null; + let tooltipFadeTimeout: ReturnType | null = null; + let tooltipHideTimeout: ReturnType | null = null; + let showCopyTooltip = false; + let isTooltipFading = false; + let showHoverTooltip = false; let copyStatusMessage = ''; - let hasInputs = false; $: has3 = ['3col', '3col-mul'].includes(config.type) || !!config.labels.in3; $: isTextInput = ['base', 'text-bin', 'bin-text', 'dec-frac', 'dms-dd', 'dd-dms'].includes(config.type); @@ -86,13 +90,49 @@ return shareUrl.toString(); } + async function copyText(text: string) { + if (navigator.clipboard?.writeText) { + await navigator.clipboard.writeText(text); + return; + } + + const textArea = document.createElement('textarea'); + textArea.value = text; + textArea.setAttribute('readonly', ''); + textArea.style.position = 'absolute'; + textArea.style.left = '-9999px'; + document.body.appendChild(textArea); + textArea.select(); + + const copied = document.execCommand('copy'); + document.body.removeChild(textArea); + if (!copied) { + throw new Error('execCommand copy failed'); + } + } + + function triggerCopyTooltip() { + if (tooltipFadeTimeout) clearTimeout(tooltipFadeTimeout); + if (tooltipHideTimeout) clearTimeout(tooltipHideTimeout); + showCopyTooltip = true; + isTooltipFading = false; + tooltipFadeTimeout = setTimeout(() => { + isTooltipFading = true; + }, 900); + tooltipHideTimeout = setTimeout(() => { + showCopyTooltip = false; + isTooltipFading = false; + }, 1300); + } + async function copyLink() { if (!browser) return; const url = buildShareUrl(); try { - await navigator.clipboard.writeText(url); + await copyText(url); copyStatus = 'copied'; + triggerCopyTooltip(); } catch (error) { console.error('Failed to copy link', error); copyStatus = 'failed'; @@ -106,12 +146,6 @@ } } - $: hasInputs = - Boolean( - val1.trim() || - val2.trim() || - (has3 && val3.trim()) - ); $: copyStatusMessage = copyStatus === 'copied' ? 'Link copied to clipboard' @@ -126,6 +160,12 @@ else if (params.has('v3') && has3) { val3 = params.get('v3')!; handleInput(3); } setTimeout(() => { paramsInitializing = false; }, 0); }); + + onDestroy(() => { + if (statusTimeout) clearTimeout(statusTimeout); + if (tooltipFadeTimeout) clearTimeout(tooltipFadeTimeout); + if (tooltipHideTimeout) clearTimeout(tooltipHideTimeout); + });
@@ -205,20 +245,29 @@ type="button" class="icon-btn" on:click={copyLink} + on:mouseenter={() => (showHoverTooltip = true)} + on:mouseleave={() => (showHoverTooltip = false)} + on:focus={() => (showHoverTooltip = true)} + on:blur={() => (showHoverTooltip = false)} aria-label="Copy calculator link" - disabled={!hasInputs} > + {#if showHoverTooltip && !showCopyTooltip} + Copy link + {/if} + {#if showCopyTooltip && copyStatus === 'copied'} + Link copied! + {/if} {copyStatusMessage} @@ -414,6 +463,33 @@ border-color: var(--accent); background: var(--surface-hover); } + .copy-tooltip { + position: absolute; + top: calc(100% + 0.4rem); + right: 0; + background: color-mix(in srgb, var(--accent) 90%, black 10%); + color: #fff; + border-radius: 6px; + padding: 0.35rem 0.55rem; + font-size: 0.72rem; + font-weight: 600; + letter-spacing: 0.02em; + pointer-events: none; + opacity: 1; + transform: translateY(0); + transition: opacity 0.35s ease, transform 0.35s ease; + white-space: nowrap; + z-index: 2; + } + .copy-tooltip.hover { + background: var(--section-bg); + color: var(--text); + border: 1px solid var(--border); + } + .copy-tooltip.fading { + opacity: 0; + transform: translateY(-0.2rem); + } .sr-only { position: absolute; width: 1px;