diff --git a/app/contributors/ContributorsSearch.empty-fallback.test.tsx b/app/contributors/ContributorsSearch.empty-fallback.test.tsx index 7e5da6cdb..729110deb 100644 --- a/app/contributors/ContributorsSearch.empty-fallback.test.tsx +++ b/app/contributors/ContributorsSearch.empty-fallback.test.tsx @@ -4,12 +4,18 @@ import { describe, expect, it } from 'vitest'; import ContributorsSearch from './ContributorsSearch'; +function expectContributorsCount(container: HTMLElement, filtered: number, total: number) { + expect(container.textContent?.replace(/\s+/g, '').trim()).toContain( + `${filtered}/${total}contributors` + ); +} + describe('ContributorsSearch empty fallback', () => { it('renders the fallback when the contributor collection is missing', () => { - render(); + const { container } = render(); expect(screen.getByText('No architects found')).toBeTruthy(); - expect(screen.getByText('0 of 0 contributors')).toBeTruthy(); + expectContributorsCount(container, 0, 0); }); it('renders no contributor profile links for an empty collection', () => { @@ -21,15 +27,15 @@ describe('ContributorsSearch empty fallback', () => { it('keeps the empty collection stable while searching and clearing', async () => { const user = userEvent.setup(); - render(); + const { container } = render(); const input = screen.getByRole('textbox', { name: 'Search contributors by name' }); await user.type(input, 'missing contributor'); expect(screen.getByText('No architects found')).toBeTruthy(); - expect(screen.getByText('0 of 0 contributors')).toBeTruthy(); + expectContributorsCount(container, 0, 0); - await user.click(screen.getByRole('button', { name: 'Clear' })); + await user.click(screen.getByRole('button', { name: 'Clear search' })); expect(input).toHaveValue(''); expect(screen.getByText('No architects found')).toBeTruthy(); @@ -37,7 +43,7 @@ describe('ContributorsSearch empty fallback', () => { it('moves from populated results to the fallback and back', async () => { const user = userEvent.setup(); - render( + const { container } = render( { expect(screen.getByText('No architects found')).toBeTruthy(); expect(screen.queryByRole('link')).toBeNull(); - await user.click(screen.getByRole('button', { name: 'Clear' })); + await user.click(screen.getByRole('button', { name: 'Clear search' })); expect(screen.getByRole('link', { name: /alice/i })).toBeTruthy(); - expect(screen.getByText('1 of 1 contributors')).toBeTruthy(); + expectContributorsCount(container, 1, 1); }); }); diff --git a/app/contributors/ContributorsSearch.theme-contrast.test.tsx b/app/contributors/ContributorsSearch.theme-contrast.test.tsx index df1c27811..8ca552897 100644 --- a/app/contributors/ContributorsSearch.theme-contrast.test.tsx +++ b/app/contributors/ContributorsSearch.theme-contrast.test.tsx @@ -62,7 +62,7 @@ describe('ContributorsSearch Theme Contrast Tests', () => { expect(input.className).toContain('dark:text-white'); - expect(input.className).toContain('dark:placeholder:text-zinc-600'); + expect(input.className).toContain('dark:placeholder:text-zinc-500'); }); it('applies dark and light border styling on contributor cards', () => { diff --git a/app/contributors/ContributorsSearch.tsx b/app/contributors/ContributorsSearch.tsx index bb7752826..cd2d7ef47 100644 --- a/app/contributors/ContributorsSearch.tsx +++ b/app/contributors/ContributorsSearch.tsx @@ -3,7 +3,7 @@ import { useState, useRef } from 'react'; import Link from 'next/link'; import Image from 'next/image'; -import { GitFork, Search } from 'lucide-react'; +import { GitFork, Search, X } from 'lucide-react'; import { motion, AnimatePresence, Variants } from 'framer-motion'; interface Contributor { @@ -89,32 +89,40 @@ export default function ContributorsSearch({ return ( <> {/* SEARCH BAR */} -
+
- {/* Animated gradient border */} -
-
- + {/* Hover/focus glow ring */} +
+
+
+ +
setSearch(e.target.value)} - className="w-full bg-transparent px-4 py-5 text-lg text-black dark:text-white placeholder:text-zinc-400 dark:placeholder:text-zinc-600 outline-none font-light" + className="w-full bg-transparent px-4 py-3.5 text-base text-black dark:text-white placeholder:text-zinc-400 dark:placeholder:text-zinc-500 outline-none focus-visible:outline-none font-medium tracking-wide" /> {search && ( )}
-
- {filtered.length} of {contributors.length} contributors +
+ + {filtered.length} + / + {contributors.length} + contributors +
diff --git a/app/contributors/page.empty-fallback.test.tsx b/app/contributors/page.empty-fallback.test.tsx index d1c52453d..7a8574fe0 100644 --- a/app/contributors/page.empty-fallback.test.tsx +++ b/app/contributors/page.empty-fallback.test.tsx @@ -88,10 +88,10 @@ describe('ContributorsPage empty fallback', () => { it('renders fallback UI when contributors are empty', async () => { const element = await ContributorsPage(); - render(element); + const { container } = render(element); expect(screen.getByText(/No architects found/i)).toBeTruthy(); - expect(screen.getByText(/0 of 0 contributors/i)).toBeTruthy(); + expect(container.textContent?.replace(/\s+/g, '')).toContain('0/0contributors'); expect(screen.getByRole('heading', { name: /THE COLLECTIVE/i })).toBeTruthy(); expect(screen.getByText(/READY TO BUILD\?/i)).toBeTruthy(); }); @@ -109,10 +109,10 @@ describe('ContributorsPage empty fallback', () => { it('handles fetch failures gracefully and still renders fallback state', async () => { global.fetch = vi.fn(() => Promise.reject(new Error('Network failure'))) as any; const element = await ContributorsPage(); - render(element); + const { container } = render(element); expect(screen.getByText(/No architects found/i)).toBeTruthy(); - expect(screen.getByText(/0 of 0 contributors/i)).toBeTruthy(); + expect(container.textContent?.replace(/\s+/g, '')).toContain('0/0contributors'); }); it('handles non-ok API responses without breaking the page', async () => { @@ -126,10 +126,10 @@ describe('ContributorsPage empty fallback', () => { ) as any; const element = await ContributorsPage(); - render(element); + const { container } = render(element); expect(screen.getByText(/No architects found/i)).toBeTruthy(); - expect(screen.getByText(/0 of 0 contributors/i)).toBeTruthy(); + expect(container.textContent?.replace(/\s+/g, '')).toContain('0/0contributors'); }); it('does not emit console errors when the fallback page renders', async () => { diff --git a/app/globals.css b/app/globals.css index 4e4cbb053..31486db0f 100644 --- a/app/globals.css +++ b/app/globals.css @@ -379,14 +379,16 @@ body { } /* ── Accessibility: visible focus ring ── */ -:focus-visible { - outline: 2px solid #58a6ff; - outline-offset: 2px; - border-radius: 4px; -} +@layer base { + :focus-visible { + outline: 2px solid #58a6ff; + outline-offset: 2px; + border-radius: 4px; + } -:focus:not(:focus-visible) { - outline: none; + :focus:not(:focus-visible) { + outline: none; + } } /* ── Accessibility: reduced motion ── */