UI Enhancements: ShinyText and PixelBlast#1
Conversation
|
@ngmkhoi is attempting to deploy a commit to the Thieu Nguyen's projects Team on Vercel. A member of the Team first needs to authorize it. |
There was a problem hiding this comment.
Pull request overview
This PR enhances the landing page UI by introducing animated “shiny” headings and interactive spotlight/pixel background effects, enabled via React components inside the Astro site.
Changes:
- Added new React components:
ShinyText,SpotlightCard, and a WebGL-basedPixelBlast. - Updated multiple landing-page sections to use the new animated text and spotlight card wrappers.
- Updated Tailwind/global styling and added React/Tailwind-related dependencies + Astro React integration.
Reviewed changes
Copilot reviewed 16 out of 17 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
| src/styles/global.css | Adds Tailwind animate plugin usage and introduces theme/CSS variable definitions. |
| src/lib/utils.ts | Adds cn() utility for class merging (clsx + tailwind-merge). |
| src/layouts/GuidesLayout.astro | Formatting-only adjustments (quote/style changes) + canonical string formatting. |
| src/components/sections/Problem.astro | Replaces cards/title styling with SpotlightCard + ShinyText. |
| src/components/sections/Pricing.astro | Uses SpotlightCard and ShinyText in pricing section. |
| src/components/sections/Hero.astro | Uses ShinyText for the hero headline. |
| src/components/sections/Features.astro | Uses ShinyText for the section header. |
| src/components/sections/Commands.astro | Uses SpotlightCard and ShinyText for commands section UI. |
| src/components/sections/ClaudeKit.astro | Uses SpotlightCard and ShinyText for ClaudeKit section UI. |
| src/components/layout/AmbientBackground.astro | Swaps static aurora blobs for PixelBlast background. |
| src/components/SpotlightCard.tsx | New React spotlight-hover wrapper component. |
| src/components/ShinyText.tsx | New React animated gradient text component. |
| src/components/PixelBlast.tsx | New Three.js/postprocessing pixel shader background component. |
| astro.config.mjs | Adds @astrojs/react integration. |
| package.json | Adds React + animation/WebGL dependencies (motion, three, postprocessing, etc.). |
| package-lock.json | Locks the newly added dependencies. |
| components.json | Adds shadcn/ui config + aliases. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| "dependencies": { | ||
| "@astrojs/react": "^4.4.2", | ||
| "@fontsource/dm-sans": "^5.2.8", | ||
| "@fontsource/fira-code": "^5.2.7", | ||
| "@fontsource/space-grotesk": "^5.2.10", | ||
| "@types/react": "^19.2.14", | ||
| "@types/react-dom": "^19.2.3", | ||
| "@vercel/analytics": "^1.6.1", |
There was a problem hiding this comment.
@types/react and @types/react-dom are listed under dependencies. These are TypeScript-only type packages and typically belong in devDependencies so they aren’t installed/packed for production deploys. Move them to devDependencies (and keep runtime deps limited to code needed at runtime).
| const { class: className = "" } = Astro.props; | ||
| import PixelBlast from "@/components/PixelBlast"; |
There was a problem hiding this comment.
import PixelBlast ... appears after executable code (const { class: ... } = Astro.props;). In Astro frontmatter (ESM), imports must be at the top level before other statements, otherwise this will fail to parse/build. Move the import above the const destructuring (and keep all imports grouped at the top).
| const { class: className = "" } = Astro.props; | |
| import PixelBlast from "@/components/PixelBlast"; | |
| import PixelBlast from "@/components/PixelBlast"; | |
| const { class: className = "" } = Astro.props; |
| <div | ||
| class={`fixed inset-0 pointer-events-none overflow-hidden aurora-bg ${className}`} | ||
| > | ||
| <PixelBlast | ||
| client:only="react" | ||
| variant="square" | ||
| pixelSize={4} | ||
| color="#2070e7" | ||
| patternScale={2} | ||
| patternDensity={1} | ||
| pixelSizeJitter={0} | ||
| enableRipples | ||
| rippleSpeed={0.4} |
There was a problem hiding this comment.
The wrapper div uses pointer-events-none, but PixelBlast attaches pointerdown/pointermove listeners to its canvas to drive ripples. With pointer events disabled at the parent, the interaction handlers will never fire. Consider removing pointer-events-none or overriding it (e.g., pointer-events-auto) on the PixelBlast element/canvas so ripples work as intended.
| <h2 class="text-4xl md:text-5xl font-heading font-bold mb-6"> | ||
| {t('pricing.section_title')} | ||
| <span class="block bg-gradient-to-r from-green-600 to-blue-600 dark:from-green-400 dark:to-blue-400 bg-clip-text text-transparent"> | ||
| {t('pricing.section_subtitle')} | ||
| </span> | ||
| <div class="mb-2"> | ||
| <ShinyText |
There was a problem hiding this comment.
<h2> contains block-level <div> wrappers around the title/subtitle. This is invalid HTML for headings and can negatively affect accessibility/semantics. Prefer <span> (styled as block) inside the heading, or place the wrapper <div>s outside the <h2>.
| <h1 | ||
| class="font-heading text-5xl md:text-7xl font-bold mb-6 leading-tight fade-in-up" | ||
| style="animation-delay: 0.1s;" | ||
| > | ||
| <div class="mb-2"> | ||
| <ShinyText |
There was a problem hiding this comment.
<h1> contains block-level <div> children. This is invalid heading markup and can break semantic structure for assistive tech. Replace the inner <div>s with <span>s (and apply block/margins via classes) or move the wrappers outside the <h1>.
| // plugins: [], | ||
| // }; |
There was a problem hiding this comment.
There are stray commented-out lines (// plugins: [], etc.) at the bottom of this component file. These don’t appear to be relevant to ShinyText and should be removed to keep the component source clean.
| // plugins: [], | |
| // }; |
| return () => { | ||
| if (threeRef.current && mustReinit) return; | ||
| if (!threeRef.current) return; | ||
| const t = threeRef.current; | ||
| t.resizeObserver?.disconnect(); | ||
| cancelAnimationFrame(t.raf!); | ||
| t.quad?.geometry.dispose(); | ||
| t.material.dispose(); | ||
| t.composer?.dispose(); | ||
| t.renderer.dispose(); | ||
| if (t.renderer.domElement.parentElement === container) container.removeChild(t.renderer.domElement); | ||
| threeRef.current = null; | ||
| }; |
There was a problem hiding this comment.
The effect cleanup returns early when mustReinit is true (if (threeRef.current && mustReinit) return;). On initial mount mustReinit is true, so on unmount this cleanup will skip disposing the WebGL renderer/composer/canvas, leaking GPU resources and leaving RAF running. Cleanup should always dispose on unmount; if the goal is to avoid double-dispose during re-init, gate that differently (e.g., track a separate flag or always dispose in cleanup and remove the manual dispose at re-init).
| <h2 class="text-4xl md:text-5xl font-heading font-bold mb-6"> | ||
| {t('problem.title')} | ||
| <span class="block bg-gradient-to-r from-red-600 to-orange-600 dark:from-red-400 dark:to-orange-400 bg-clip-text text-transparent"> | ||
| {t('problem.subtitle')} | ||
| </span> | ||
| <div class="mb-2"> | ||
| <ShinyText |
There was a problem hiding this comment.
<h2> contains block-level <div> children. This is invalid HTML (headings should only contain phrasing content), and can confuse screen readers / document outline. Use <span> wrappers (display:block via CSS) or move the block wrappers outside the heading.
| setPosition({ x: e.clientX - rect.left, y: e.clientY - rect.top }); | ||
| }; | ||
|
|
||
| const handleFocus = () => { |
There was a problem hiding this comment.
On keyboard focus, handleFocus sets opacity but never updates position, so the spotlight will render at the default (0,0) until the user moves the mouse. Consider setting position on focus (e.g., center of the card or based on the focused element’s bounding box) so keyboard users get a sensible highlight location.
| const handleFocus = () => { | |
| const handleFocus = () => { | |
| if (divRef.current) { | |
| const rect = divRef.current.getBoundingClientRect(); | |
| setPosition({ x: rect.width / 2, y: rect.height / 2 }); | |
| } |
| import { clsx, type ClassValue } from "clsx" | ||
| import { twMerge } from "tailwind-merge" | ||
|
|
||
| export function cn(...inputs: ClassValue[]) { | ||
| return twMerge(clsx(inputs)) |
There was a problem hiding this comment.
src/lib/utils.ts uses double quotes and omits semicolons, which is inconsistent with the prevailing TS style in this repo (e.g. src/i18n/utils.ts and src/pages/robots.txt.ts use single quotes + semicolons). Align this new utility file with the existing formatting to avoid style drift/noisy diffs.
| import { clsx, type ClassValue } from "clsx" | |
| import { twMerge } from "tailwind-merge" | |
| export function cn(...inputs: ClassValue[]) { | |
| return twMerge(clsx(inputs)) | |
| import { clsx, type ClassValue } from 'clsx'; | |
| import { twMerge } from 'tailwind-merge'; | |
| export function cn(...inputs: ClassValue[]) { | |
| return twMerge(clsx(inputs)); |
Added animated shiny text effects and PixelBlast component to improve landing page aesthetics.