') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); Feat/code blocks with copy by Krish-Parekh · Pull Request #303 · usesend/useSend · GitHub
Skip to content

Feat/code blocks with copy - #303

Merged
KMKoushik merged 2 commits into
usesend:mainfrom
Krish-Parekh:feat/code-blocks-with-copy
Dec 7, 2025
Merged

Feat/code blocks with copy#303
KMKoushik merged 2 commits into
usesend:mainfrom
Krish-Parekh:feat/code-blocks-with-copy

Conversation

@Krish-Parekh

@Krish-ParekhKrish-Parekh commented Dec 7, 2025

Copy link
Copy Markdown
Contributor

Summary by cubic

Adds a reusable code block with a copy button and integrates it into the marketing CodeExample for one‑click code copying with clear feedback. Also removes unintended italics in Shiki-rendered code for consistent readability.

  • New Features

    • Introduces CodeBlockWithCopy with an overlay copy button and success state.
    • Wraps CodeBlock in marketing CodeExample to enable copy per language tab.
  • Bug Fixes

    • Prevents italic styling in Shiki code blocks (light and dark).

Written for commit 9d5b547. Summary will update automatically on new commits.

Summary by CodeRabbit

Release Notes

  • New Features

    • Code blocks now feature an integrated copy button that copies content to clipboard with visual feedback.
  • Bug Fixes

    • Fixed unwanted italic text styling in code blocks.

✏️ Tip: You can customize this high-level summary in your review settings.

Krish-Parekhand others added 2 commits December 5, 2025 07:41
…for enhanced code copying functionality; update styles to prevent italic in Shiki code blocks
@vercel

vercelBot commented Dec 7, 2025

Copy link
Copy Markdown

@Krish-Parekh is attempting to deploy a commit to the kmkoushik's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitaiBot commented Dec 7, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

This pull request introduces copy-to-clipboard functionality for code blocks across the application. A new CodeBlockWithCopy React component is added to the UI package that wraps code blocks with a copy button. The CodeExample component in the marketing app is updated to use this new wrapper component. Additionally, global CSS styling is modified to prevent italic formatting in Shiki code blocks by applying a consistent font-style reset.

Pre-merge checks

❌ Failed checks (1 warning)
Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 0.00% which is insufficient. The required threshold is 80.00%.You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title 'Feat/code blocks with copy' clearly and concisely describes the main change: adding copy functionality to code blocks across the codebase.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
packages/ui/src/code-block-with-copy.tsx (1)

3-7: Solid copy-to-clipboard implementation with a few optional refinements

The component correctly encapsulates copy-to-clipboard behavior for arbitrary code blocks: props are well-typed, navigator.clipboard.writeText(code) is safely wrapped in try/catch, and the Button is positioned and labeled appropriately for basic accessibility. No blocking issues here.

A couple of small, optional tweaks you might consider:

  1. Avoid state updates after unmount / manage repeated clicks

    Right now each click schedules a new setTimeout without cleanup. It’s very low risk given the 2s window, but you can make this more robust by tracking and clearing the timeout:

-import * as React from "react";+import * as React from "react";
@@
- const [isCopied, setIsCopied] = React.useState(false);+ const [isCopied, setIsCopied] = React.useState(false);+ const resetTimeoutRef = React.useRef<number | null>(null);
const copyToClipboard = async () => {
try {
- await navigator.clipboard.writeText(code);- setIsCopied(true);- setTimeout(() => setIsCopied(false), 2000);+ await navigator.clipboard.writeText(code);+ setIsCopied(true);++ if (resetTimeoutRef.current) {+ window.clearTimeout(resetTimeoutRef.current);+ }++ resetTimeoutRef.current = window.setTimeout(() => {+ setIsCopied(false);+ resetTimeoutRef.current = null;+ }, 2000);
} catch (err) {
console.error("Failed to copy: ", err);
}
};
++ React.useEffect(+ () => () => {+ if (resetTimeoutRef.current) {+ window.clearTimeout(resetTimeoutRef.current);+ }+ },+ []+ );
  1. Minor import cleanup

    You can consolidate the lucide-react imports for tidiness:

-import { CheckIcon } from "lucide-react";+import { CheckIcon, Copy } from "lucide-react";
@@
-import { Copy } from "lucide-react";
  1. Optional discoverability/accessibility tweak

    Since the button is opacity-0 until group-hover, users on touch-only devices may not see it. If that’s a concern, you could default to opacity-100 on small screens and only use hover-based fade on larger breakpoints, or also force visibility on focus-visible so keyboard users get a clear affordance.

Also applies to: 19-29, 31-50

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b4640ac and 9d5b547.

📒 Files selected for processing (3)
  • apps/marketing/src/components/CodeExample.tsx (2 hunks)
  • packages/ui/src/code-block-with-copy.tsx (1 hunks)
  • packages/ui/styles/globals.css (1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{tsx,ts,jsx,js}

📄 CodeRabbit inference engine (.cursor/rules/general.mdc)

Include all required imports and ensure proper naming of key components in React/NextJS code

Files:

  • apps/marketing/src/components/CodeExample.tsx
  • packages/ui/src/code-block-with-copy.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,js,jsx}: Use TypeScript-first approach with 2-space indent and semicolons enabled by Prettier in apps/web (Next.js), apps/marketing, apps/smtp-server, and all packages
Never use dynamic imports; always import on the top level
Run ESLint via @usesend/eslint-config and ensure no warnings remain before submitting PRs

Files:

  • apps/marketing/src/components/CodeExample.tsx
  • packages/ui/src/code-block-with-copy.tsx
**/*.{tsx,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

React components must use PascalCase naming convention (e.g., AppSideBar.tsx)

Files:

  • apps/marketing/src/components/CodeExample.tsx
  • packages/ui/src/code-block-with-copy.tsx
**/*.{ts,tsx,md}

📄 CodeRabbit inference engine (AGENTS.md)

Run Prettier 3 for code formatting on TypeScript, TSX, and Markdown files

Files:

  • apps/marketing/src/components/CodeExample.tsx
  • packages/ui/src/code-block-with-copy.tsx
🧠 Learnings (1)
📚 Learning: 2025-11-28T21:13:56.758Z
Learnt from: CR
Repo: usesend/useSend PR: 0
File: .cursor/rules/general.mdc:0-0
Timestamp: 2025-11-28T21:13:56.758Z
Learning: Applies to **/*.{tsx,ts,jsx,js} : Include all required imports and ensure proper naming of key components in React/NextJS code

Applied to files:

  • apps/marketing/src/components/CodeExample.tsx
🧬 Code graph analysis (1)
packages/ui/src/code-block-with-copy.tsx (2)
packages/email-editor/src/renderer.tsx (1)
  • code (740-756)
packages/ui/src/button.tsx (1)
  • Button (80-80)
🔇 Additional comments (2)
packages/ui/styles/globals.css (1)

142-146: Shiki font-style reset aligns with the intended typography change

The global .shiki, .shiki span { font-style: normal !important; } cleanly enforces non-italic code tokens across themes while letting the dark-mode block still control colors and other text styling. This matches the PR goal and doesn’t introduce obvious regressions.

apps/marketing/src/components/CodeExample.tsx (1)

3-3: CodeBlockWithCopy integration in CodeExample looks correct

The new import and the CodeBlockWithCopy wrapper both line up with the component’s API: code={l.code} is a string, and the inner CodeBlock still receives the same lang and children, so highlighting behavior is preserved while adding copy-to-clipboard. Naming and top-level import usage also follow the repo’s TSX/React guidelines.

Also applies to: 153-160

@cubic-dev-aicubic-dev-aiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 3 files

@KMKoushik
KMKoushik merged commit 641d5f3 into usesend:mainDec 7, 2025
4 of 5 checks passed
@coderabbitaicoderabbitaiBot mentioned this pull request Jan 4, 2026
Sign up for freeto 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.

2 participants

@Krish-Parekh@KMKoushik