diff --git a/src/uix/eidos/components/tooltip/tooltip.css b/src/uix/eidos/components/tooltip/tooltip.css index e8affad41..75d5f0f4f 100644 --- a/src/uix/eidos/components/tooltip/tooltip.css +++ b/src/uix/eidos/components/tooltip/tooltip.css @@ -96,7 +96,7 @@ * registered `motion` preset. */ [data-tooltip-content][data-state='delayed-open'] { - animation: eidos-tooltip-bounce-in var(--duration-moderate) var(--ease-out) both; + animation: eidos-tooltip-bounce-in var(--duration-slow) var(--ease-out) both; } [data-tooltip-content][data-state='closed'] { animation: eidos-tooltip-bounce-out var(--duration-fast) var(--ease-in) both; @@ -108,14 +108,15 @@ transform: none; } -/* A real bounce: grow past 1 (overshoot 1.06), dip slightly under (undershoot - * 0.97), settle to 1. Subtle in absolute terms (a tooltip is small) but clearly a - * "rebote". `scale` (the individual property, not the `transform` shorthand) +/* A real bounce (muelle): grow from half size, overshoot clearly past 1, dip + * under, a smaller second overshoot, then settle — a perceptible spring, not a + * subtle pop. `scale` (the individual property, not the `transform` shorthand) * composes with `transform-origin` and never clobbers a sibling transform. */ @keyframes eidos-tooltip-bounce-in { - 0% { opacity: 0; scale: 0.7; } - 50% { opacity: 1; scale: 1.06; } - 74% { scale: 0.97; } + 0% { opacity: 0; scale: 0.5; } + 38% { opacity: 1; scale: 1.18; } + 60% { scale: 0.9; } + 80% { scale: 1.06; } 100% { scale: 1; } } @keyframes eidos-tooltip-bounce-out { diff --git a/src/uix/soma/components/tooltip/tooltip-provider.svelte.test.ts b/src/uix/soma/components/tooltip/tooltip-provider.svelte.test.ts index 6c8fb291a..14aa40ec7 100644 --- a/src/uix/soma/components/tooltip/tooltip-provider.svelte.test.ts +++ b/src/uix/soma/components/tooltip/tooltip-provider.svelte.test.ts @@ -151,8 +151,9 @@ describe('TooltipProvider', () => { harness.tasks.get('soma:tooltip:tooltip-root:open')?.(); await tick(); expect(opts.open.current).toBe(true); - expect(result.provider.tooltipState).toBe('instant-open'); - expect(triggerEl.getAttribute('data-state')).toBe('instant-open'); + // Hover-delay path opens with the ENTRANCE state so the eidos animation plays. + expect(result.provider.tooltipState).toBe('delayed-open'); + expect(triggerEl.getAttribute('data-state')).toBe('delayed-open'); result.trigger.onpointerleave(); expect(harness.schedule).toHaveBeenCalledWith( diff --git a/src/uix/soma/components/tooltip/tooltip-provider.svelte.ts b/src/uix/soma/components/tooltip/tooltip-provider.svelte.ts index 0e29db26d..19c9b9a18 100644 --- a/src/uix/soma/components/tooltip/tooltip-provider.svelte.ts +++ b/src/uix/soma/components/tooltip/tooltip-provider.svelte.ts @@ -220,7 +220,12 @@ export class TooltipProvider { this.delay, () => { this.openTimer = null; - this.internalTooltipState = 'instant-open'; + // Stay 'delayed-open' (NOT 'instant-open') as the content MOUNTS, so the + // eidos entrance animation keyed on `[data-state='delayed-open']` actually + // plays. The content only mounts here (open → true), so setting + // 'instant-open' meant the hover entrance NEVER fired. 'instant-open' stays + // reserved for focus / skip-delay (no entrance). + this.internalTooltipState = 'delayed-open'; this.opts.open.current = true; }, { replace: true, meta: { component: 'tooltip', action: 'open-delay' } }