Skip to content

feat(cdn): sync blink animation with React constants; update billing copy#427

Merged
bensonwong merged 5 commits intomainfrom
feat/cdn-blink-constants-and-billing-copy
Apr 15, 2026
Merged

feat(cdn): sync blink animation with React constants; update billing copy#427
bensonwong merged 5 commits intomainfrom
feat/cdn-blink-constants-and-billing-copy

Conversation

@bensonwong
Copy link
Copy Markdown
Collaborator

Summary

  • CDN blink animation refactor: Imports BLINK_* timing/easing/opacity/scale constants from src/react/constants.ts instead of local redefinitions. CDN and React now share identical values — the old CDN had BLINK_ENTER_DURATION_MS = 180ms vs React's correct 120ms.
  • Three-stage enter animation: Implements the Blink Standard Pattern (enter-a instant → enter-b settle → steady) matching useBlinkMotionStage, using translate3d + will-change for GPU compositor hints. Replaces the previous single-transition approach.
  • cancelBlink() guard: Cancels any in-flight RAF + timers before starting a new open/close, preventing overlapping sequences. Also called in hidePopoverCleanup() to prevent orphaned timers.
  • Billing copy update: Updates quota-exceeded error message and billing dashboard description to reflect the subscription tier model (Standard 20/week at $20/mo, Pro unlimited at $200/mo), replacing the old pay-as-you-go copy.
  • Simplification: Extracts blinkTransition() helper to deduplicate three identical opacity Xms E, transform Xms E template strings. Types blinkRafId as number | null for consistency. Clears willChange in cleanup.

Test plan

  • Open a CDN popover — verify enter animation is smooth (subtle scale + opacity settle, no directional translation)
  • Close a CDN popover — verify exit animation completes before cleanup
  • Rapidly open/close — verify no animation artifacts or orphaned timers
  • Trigger quota-exceeded error via CLI — verify updated message shows tier names and upgrade URL
  • Run npx deepcitation billing — verify updated dashboard description
  • bun run lint passes (verified locally)

Benson and others added 3 commits April 14, 2026 17:21
Replace pay-as-you-go pricing copy with subscription tier messaging
(Standard 20/week at $20/mo, Pro unlimited at $200/mo) in both the
quota-reached error and the billing dashboard description.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… enter

- Import BLINK_* constants from constants.ts instead of redefining locally;
  CDN and React now share identical timing/easing values.
- Implement three-stage enter (enter-a instant → enter-b settle → steady)
  matching useBlinkMotionStage, using translate3d + will-change for GPU
  compositor hints.
- Add cancelBlink() to guard against overlapping open/close sequences.
- Call cancelBlink() in hidePopoverCleanup() to prevent orphaned timers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Extract blinkTransition() to eliminate 3 duplicate template-literal
  transition strings in animateOpen/animateClose.
- Type blinkRafId as number | null for consistency with settle/final timers.
- Clear willChange in hidePopoverCleanup() for complete style reset.
- Null blinkRafId in cancelBlink() after cancelling (mirrors timer pattern).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 14, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

4 Skipped Deployments
Project Deployment Actions Updated (UTC)
agui-chat-deepcitation Ignored Ignored Preview Apr 15, 2026 0:32am
deepcitation-langchain-rag-chat Ignored Ignored Preview Apr 15, 2026 0:32am
mastra-rag-deepcitation Ignored Ignored Preview Apr 15, 2026 0:32am
nextjs-ai-sdk-deepcitation Ignored Ignored Preview Apr 15, 2026 0:32am

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 14, 2026

✅ Playwright Test Report

Status: Tests passed

📊 Download Report & Snapshots (see Artifacts section)

What's in the Visual Snapshots

The gallery includes visual snapshots for:

  • 🖥️ Desktop showcase (all variants × all states)
  • 📱 Mobile showcase (iPhone SE viewport)
  • 📟 Tablet showcase (iPad viewport)
  • 🔍 Popover states (verified, partial, not found)
  • 🔗 URL citation variants

Run ID: 24430050432

@claude
Copy link
Copy Markdown

claude bot commented Apr 14, 2026

Code Review

Overall this is a solid PR — the constant consolidation is the right call and the three-stage blink pattern mirrors useBlinkMotionStage faithfully. A few issues worth addressing before merge.


🔴 Failing tests (must fix)

The billing copy changes in cliUtils.ts don't update the corresponding tests in src/__tests__/cliUnit.test.ts. These three test cases will break:

cliUnit.test.ts:108

expect(result).toContain("Payment required"); // now says "Quota reached"

cliUnit.test.ts:111

expect(result).toContain("$0.05/doc"); // removed from output

cliUnit.test.ts:201-202

expect(result).toContain("Pay-as-you-go"); // removed
expect(result).toContain("spend cap");      // removed

These need to be updated to match the new strings ("Quota reached", "Standard", "20 verifications/week", etc.), or the new copy needs to be reflected in new test cases.


🟡 onDone can be silently dropped in back-to-back animateClose calls

Before this PR, animateClose used an anonymous local setTimeout, so two in-flight close animations had independent timers — both onDone callbacks would eventually fire.

After this PR, both use the shared blinkFinalTimer. If animateClose is called while a previous close animation is still in flight (e.g. hidePopoverInner is triggered before isOpen flips to false in hidePopoverCleanup), the second call's cancelBlink() silently cancels the first timer and its onDone is never called:

// animateClose call #1 — blinkFinalTimer = Timer-A (calls hidePopoverCleanup)
// animateClose call #2 — cancelBlink() cancels Timer-A; blinkFinalTimer = Timer-B
// Timer-A's onDone (hidePopoverCleanup) is dropped ← regression

The if (!isOpen) return guard in hidePopoverInner helps here, but isOpen isn't reset synchronously — it's reset inside hidePopoverCleanup which runs after the animation completes. The safe fix is to either:

  • Reset isOpen = false synchronously at the top of hidePopoverInner, or
  • Use a dedicated blinkExitTimer separate from blinkFinalTimer (which is currently dual-purpose: willChange cleanup and exit callback)

The dual use of blinkFinalTimer is also the root of a minor null-ordering issue: in animateClose, blinkFinalTimer = null runs after onDone(), meaning hidePopoverCleanup → cancelBlink() sees the non-null (already-fired) timer ID. It's a no-op but worth cleaning up:

// prefer: null before calling onDone to avoid stale reference
blinkFinalTimer = setTimeout(() => {
  blinkFinalTimer = null;
  onDone();
}, BLINK_EXIT_TOTAL_MS);

🟡 Billing copy formatting — npx deepcitation billing reads as a sub-item of the URL

  Upgrade at: https://deepcitation.com/billing
    npx deepcitation billing

The CLI command is indented one extra level under the URL, making it look like a sub-item or continuation. In the old copy the CLI command was listed first as the primary option. Consider:

  Upgrade at: ${baseUrl}/billing
  Or run:     npx deepcitation billing

✅ What's working well

  • Constant consolidation is clean. The old CDN had BLINK_ENTER_DURATION_MS = 180 vs React's 120 and a spring-like easing (cubic-bezier(0.16, 1, 0.3, 1)) vs React's cubic-bezier(0.25, 0.25, 0.5, 1). Both drifts are fixed.
  • cancelBlink() guard is correctly placed in both animateOpen, animateClose, and hidePopoverCleanup.
  • blinkTransition() helper removes the three duplicated template string patterns cleanly.
  • BLINK_SETTLE_MS / BLINK_FINAL_MS guards (Math.max(16, ...)) are appropriately defensive for future constant changes.
  • willChange lifecycle is correct: set on enter, cleared either by the willChange-cleanup timer or by hidePopoverCleanup.
  • translate3d(0, 0, 0) for GPU promotion instead of the old translateY(4px) directional shift matches the zero-Y constants (BLINK_ENTER_Y_A_PX = 0).

Summary: The CDN animation refactor is ready pending the animateClose/blinkFinalTimer concern. The billing copy changes need the test file updated before this can merge.

Benson and others added 2 commits April 14, 2026 18:24
…copy

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ion target

When a page-expand or page-collapse ghost transition fires, isViewTransitioning()
becomes true which defers CDN reposition. But waitForPage*Target polls for the
keyhole/annotation rect before the CDN repositions, finding a stable rect at the
stale (wrong) wrapper position. The ghost then animates toward the wrong target.

Fix: useLayoutEffect in CdnPopoverWrapper calls reposition() synchronously on
viewState.current changes. useLayoutEffect fires inside flushSync (after React
DOM update, before flushSync returns), so the wrapper is at the correct position
before the transition code reads any target rects.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@bensonwong bensonwong merged commit 30be2fe into main Apr 15, 2026
14 checks passed
@bensonwong bensonwong deleted the feat/cdn-blink-constants-and-billing-copy branch April 15, 2026 01:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant