diff --git a/app/(app)/search/page.tsx b/app/(app)/search/page.tsx new file mode 100644 index 0000000..ae88acf --- /dev/null +++ b/app/(app)/search/page.tsx @@ -0,0 +1,25 @@ +type SearchPageProps = { + searchParams: Promise<{ q?: string }>; +}; + +export default async function SearchPage({ searchParams }: SearchPageProps) { + const { q } = await searchParams; + + return ( +
+

Search Results

+ {q ? ( +

+ Showing results for: {q} +

+ ) : ( +

Enter a search query to begin.

+ )} + + {/* TODO: Implement search results */} +
+ Do Search on mock data (+TDD) → then Convex. +
+
+ ); +} diff --git a/app/globals.css b/app/globals.css index 0859e1a..97e844b 100644 --- a/app/globals.css +++ b/app/globals.css @@ -61,7 +61,7 @@ --color-sidebar-border: var(--sidebar-border); --color-sidebar-ring: var(--sidebar-ring); - /* Custom */ + /* Custom: Overlay */ --color-overlay: var(--overlay); } @@ -75,23 +75,23 @@ --radius: 0.625rem; /* shadcn/ui: Core */ - --background: oklch(0.99 0 0); /* #FDFDFD */ + --background: oklch(0.99 0 0); --foreground: oklch(0.129 0.042 264.695); - --card: oklch(0.9722 0.0034 247.86); /* #F4F6F8 */ - --card-foreground: oklch(0.129 0.042 264.695); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.129 0.042 264.695); + --card: oklch(0.9722 0.0034 247.86); + --card-foreground: var(--foreground); + --popover: var(--card); + --popover-foreground: var(--foreground); --primary: oklch(0.7089 0.1967 46.81); --primary-foreground: oklch(1 0 0); - --secondary: oklch(0.9147 0.0205 264.47); /* #DCE3F1 - Figma */ + --secondary: oklch(0.9147 0.0205 264.47); --secondary-foreground: oklch(0.208 0.042 265.755); --muted: oklch(0.968 0.007 247.896); --muted-foreground: oklch(0.554 0.046 257.417); - --accent: oklch(0.968 0.007 247.896); - --accent-foreground: oklch(0.208 0.042 265.755); + --accent: var(--muted); + --accent-foreground: var(--secondary-foreground); --destructive: oklch(0.577 0.245 27.325); --border: oklch(0.929 0.013 255.508); - --input: oklch(0.929 0.013 255.508); + --input: var(--border); --ring: oklch(0.704 0.04 256.788); /* shadcn/ui: Charts */ @@ -102,17 +102,17 @@ --chart-5: oklch(0.769 0.188 70.08); /* shadcn/ui: Sidebar */ - --sidebar: oklch(1 0 0); /* Figma #ffffff */ - --sidebar-foreground: oklch(0.2102 0.0185 270.39); /* #151821 */ - --sidebar-primary: oklch(0.208 0.042 265.755); + --sidebar: oklch(1 0 0); + --sidebar-foreground: oklch(0.2102 0.0185 270.39); + --sidebar-primary: var(--secondary-foreground); --sidebar-primary-foreground: oklch(0.984 0.003 247.858); - --sidebar-accent: oklch(0.968 0.007 247.896); - --sidebar-accent-foreground: oklch(0.208 0.042 265.755); - --sidebar-border: oklch(0.9722 0.0034 247.86); /* #F4F6F8 */ - --sidebar-ring: oklch(0.704 0.04 256.788); + --sidebar-accent: var(--muted); + --sidebar-accent-foreground: var(--secondary-foreground); + --sidebar-border: var(--card); + --sidebar-ring: var(--ring); /* Custom: Gradients */ - --primary-gradient-to: oklch(0.7434 0.115 58.23); /* #E2995F */ + --primary-gradient-to: oklch(0.7434 0.115 58.23); --gradient-primary: linear-gradient( 93.22deg, var(--primary) -13.95%, @@ -120,37 +120,34 @@ ); /* Custom: Overlay */ - --overlay: oklch(0 0 0); /* black, both modes */ + --overlay: oklch(0 0 0); /* Custom: Assets (theme-aware URLs) */ --logo-full-themed: url("/images/logo-light.svg"); --auth-bg: url("/images/auth-bg-light.webp"); /* Custom: Tag badges */ - --tag-bg: oklch(0.9722 0.0034 247.86); /* #F4F6F8 */ - --tag-text: oklch(0.6504 0.0475 272.34); /* #858EAD */ + --tag-bg: var(--card); + --tag-text: oklch(0.6504 0.0475 272.34); } /* Dark mode overrides — these values take precedence over :root when .dark is active */ .dark { - /* Base */ - --radius: 0.625rem; /* same as root */ - /* shadcn/ui: Core */ - --background: oklch(0 0 0); /* #000000 */ + --background: oklch(0 0 0); --foreground: oklch(0.984 0.003 247.858); - --card: oklch(0.1783 0.0128 270.6); /* #0F1117 */ - --card-foreground: oklch(0.984 0.003 247.858); - --popover: oklch(0.208 0.042 265.755); - --popover-foreground: oklch(0.984 0.003 247.858); + --card: oklch(0.1783 0.0128 270.6); + --card-foreground: var(--foreground); + --popover: oklch(0.2741 0.023 270.44); + --popover-foreground: var(--foreground); --primary: oklch(0.7089 0.1967 46.81); --primary-foreground: oklch(1 0 0); - --secondary: oklch(0.2728 0.0257 265.4); /* #212734 - Figma */ - --secondary-foreground: oklch(0.984 0.003 247.858); + --secondary: oklch(0.2728 0.0257 265.4); + --secondary-foreground: var(--foreground); --muted: oklch(0.279 0.041 260.031); --muted-foreground: oklch(0.704 0.04 256.788); - --accent: oklch(0.279 0.041 260.031); - --accent-foreground: oklch(0.984 0.003 247.858); + --accent: var(--muted); + --accent-foreground: var(--foreground); --destructive: oklch(0.704 0.191 22.216); --border: oklch(1 0 0 / 10%); --input: oklch(1 0 0 / 15%); @@ -164,33 +161,33 @@ --chart-5: oklch(0.645 0.246 16.439); /* shadcn/ui: Sidebar */ - --sidebar: oklch(0.1783 0.0128 270.6); /* Figma #0F1117 */ - --sidebar-foreground: oklch(1 0 0); /* #FFFFFF */ + --sidebar: var(--card); + --sidebar-foreground: oklch(1 0 0); --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.984 0.003 247.858); - --sidebar-accent: oklch(0.279 0.041 260.031); - --sidebar-accent-foreground: oklch(0.984 0.003 247.858); + --sidebar-primary-foreground: var(--foreground); + --sidebar-accent: var(--muted); + --sidebar-accent-foreground: var(--foreground); --sidebar-border: transparent; - --sidebar-ring: oklch(0.551 0.027 264.364); + --sidebar-ring: var(--ring); /* Custom: Gradients */ - --primary-gradient-to: oklch(0.7434 0.115 58.23); /* same as root */ + --primary-gradient-to: oklch(0.7434 0.115 58.23); --gradient-primary: linear-gradient( 93.22deg, var(--primary) -13.95%, var(--primary-gradient-to) 99.54% - ); /* same as root */ + ); /* Custom: Overlay */ - --overlay: oklch(0 0 0); /* same as root */ + --overlay: oklch(0 0 0); /* Custom: Assets (theme-aware URLs) */ --logo-full-themed: url("/images/logo-dark.svg"); --auth-bg: url("/images/auth-bg-dark.webp"); /* Custom: Tag badges */ - --tag-bg: oklch(0.2102 0.0185 270.39); /* #151821 */ - --tag-text: oklch(0.6547 0.0897 269.9); /* #7B8EC8 */ + --tag-bg: oklch(0.2102 0.0185 270.39); + --tag-text: oklch(0.6547 0.0897 269.9); } @layer base { @@ -257,11 +254,8 @@ @apply invert dark:invert-0; } - /* Subtle directional shadow for navigation edges (light mode only) */ + /* Subtle directional shadow for navigation edges */ .shadow-light { box-shadow: -10px 10px 20px 0px oklch(0.877 0.006 17.26 / 10%); } - .dark .shadow-light { - box-shadow: none; - } } diff --git a/components/navigation/desktop-topbar.tsx b/components/navigation/desktop-topbar.tsx index d79d2a8..cef4578 100644 --- a/components/navigation/desktop-topbar.tsx +++ b/components/navigation/desktop-topbar.tsx @@ -1,14 +1,14 @@ import { SignedOut, SignInButton, SignUpButton } from "@clerk/nextjs"; -import { Searchbox } from "@/components/search/searchbox"; +import { DesktopSearch } from "@/components/search/desktop-search"; import { Button } from "@/components/ui/button"; export function DesktopTopBar() { return (
- {/* Left section: matches main content structure (padding + max-w-5xl centering) */} + {/* Left section: search bar narrower than content (max-w-2xl vs content's max-w-5xl) */}
-
- +
+
diff --git a/components/navigation/mobile-topbar.tsx b/components/navigation/mobile-topbar.tsx index d9eac7e..05a9822 100644 --- a/components/navigation/mobile-topbar.tsx +++ b/components/navigation/mobile-topbar.tsx @@ -1,5 +1,6 @@ import Link from "next/link"; import { MobileNav } from "@/components/navigation/mobile-nav"; +import { MobileSearch } from "@/components/search/mobile-search"; export function MobileTopBar() { return ( @@ -10,7 +11,10 @@ export function MobileTopBar() { - +
+ + +
); } diff --git a/components/search/desktop-search.tsx b/components/search/desktop-search.tsx new file mode 100644 index 0000000..45cacac --- /dev/null +++ b/components/search/desktop-search.tsx @@ -0,0 +1,40 @@ +"use client"; + +import { SearchHints } from "@/components/search/search-hints"; +import { SearchInput } from "@/components/search/search-input"; +import { SearchPopoverContent } from "@/components/search/search-popover-content"; +import { Popover, PopoverAnchor } from "@/components/ui/popover"; +import { useSearch } from "@/hooks/use-search"; + +export function DesktopSearch() { + const { + isOpen, + query, + setQuery, + inputRef, + open, + close, + handleSubmit, + handleKeyDown, + } = useSearch(); + + return ( + !open && close()}> + +
+ + +
+ + + +
+ ); +} diff --git a/components/search/mobile-search.tsx b/components/search/mobile-search.tsx new file mode 100644 index 0000000..d7f3916 --- /dev/null +++ b/components/search/mobile-search.tsx @@ -0,0 +1,105 @@ +"use client"; + +import { Search } from "lucide-react"; +import { useEffect, useState } from "react"; +import { SearchHints } from "@/components/search/search-hints"; +import { SearchInput } from "@/components/search/search-input"; +import { SearchPopoverContent } from "@/components/search/search-popover-content"; +import { Button } from "@/components/ui/button"; +import { Popover, PopoverAnchor } from "@/components/ui/popover"; +import { useSearch } from "@/hooks/use-search"; +import { cn } from "@/lib/utils"; + +export function MobileSearch() { + const [showHints, setShowHints] = useState(false); + + const { + isOpen, + query, + setQuery, + inputRef, + open, + reset, + handleSubmit, + handleKeyDown, + } = useSearch({ + onClose: () => setShowHints(false), + }); + + // Auto-focus input when overlay opens + useEffect(() => { + if (isOpen) { + const timer = setTimeout(() => inputRef.current?.focus(), 50); + return () => clearTimeout(timer); + } + }, [isOpen, inputRef]); + + // 0.5s delay before showing hints + useEffect(() => { + if (!isOpen) { + setShowHints(false); + return; + } + + const timer = setTimeout(() => setShowHints(true), 500); + return () => clearTimeout(timer); + }, [isOpen]); + + return ( + <> + {/* Search icon trigger button */} + + + {/* Overlay and search panel (only when active) */} + {isOpen && ( + <> + {/* Backdrop overlay - click to close */} + + + + ); +} diff --git a/components/search/search-input.tsx b/components/search/search-input.tsx new file mode 100644 index 0000000..b49e85e --- /dev/null +++ b/components/search/search-input.tsx @@ -0,0 +1,55 @@ +"use client"; + +import { Search } from "lucide-react"; +import { forwardRef, type KeyboardEvent } from "react"; +import { Input } from "@/components/ui/input"; +import { cn } from "@/lib/utils"; + +type SearchInputProps = { + value: string; + onChange: (value: string) => void; + onKeyDown?: (e: KeyboardEvent) => void; + onFocus?: () => void; + placeholder?: string; + className?: string; + highlighted?: boolean; +}; + +export const SearchInput = forwardRef( + ( + { + value, + onChange, + onKeyDown, + onFocus, + placeholder = "Search...", + className, + highlighted = false, + }, + ref, + ) => { + return ( +
+ + onChange(e.target.value)} + onKeyDown={onKeyDown} + onFocus={onFocus} + className={cn( + "w-full pl-9", + highlighted && "border-primary", + className, + )} + /> +
+ ); + }, +); + +SearchInput.displayName = "SearchInput"; diff --git a/components/search/search-popover-content.tsx b/components/search/search-popover-content.tsx new file mode 100644 index 0000000..3e87dc3 --- /dev/null +++ b/components/search/search-popover-content.tsx @@ -0,0 +1,26 @@ +"use client"; + +import type * as React from "react"; +import { PopoverContent } from "@/components/ui/popover"; +import { cn } from "@/lib/utils"; + +type SearchPopoverContentProps = React.ComponentProps; + +export function SearchPopoverContent({ + className, + sideOffset = 8, + ...props +}: SearchPopoverContentProps) { + return ( + e.preventDefault()} + {...props} + /> + ); +} diff --git a/components/search/searchbox.tsx b/components/search/searchbox.tsx deleted file mode 100644 index 64f9ed8..0000000 --- a/components/search/searchbox.tsx +++ /dev/null @@ -1,5 +0,0 @@ -// TODO: Implement search similar to stackoverflow - -export function Searchbox() { - return

Searchbox

; -} diff --git a/components/ui/command.tsx b/components/ui/command.tsx deleted file mode 100644 index 397d948..0000000 --- a/components/ui/command.tsx +++ /dev/null @@ -1,183 +0,0 @@ -"use client"; - -import { Command as CommandPrimitive } from "cmdk"; -import { SearchIcon } from "lucide-react"; -import type * as React from "react"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { cn } from "@/lib/utils"; - -function Command({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function CommandDialog({ - title = "Command Palette", - description = "Search for a command to run...", - children, - className, - showCloseButton = true, - ...props -}: React.ComponentProps & { - title?: string; - description?: string; - className?: string; - showCloseButton?: boolean; -}) { - return ( - - - {title} - {description} - - - - {children} - - - - ); -} - -function CommandInput({ - className, - ...props -}: React.ComponentProps) { - return ( -
- - -
- ); -} - -function CommandList({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function CommandEmpty({ - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function CommandGroup({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function CommandSeparator({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function CommandItem({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function CommandShortcut({ - className, - ...props -}: React.ComponentProps<"span">) { - return ( - - ); -} - -export { - Command, - CommandDialog, - CommandInput, - CommandList, - CommandEmpty, - CommandGroup, - CommandItem, - CommandShortcut, - CommandSeparator, -}; diff --git a/hooks/use-search.ts b/hooks/use-search.ts new file mode 100644 index 0000000..c254772 --- /dev/null +++ b/hooks/use-search.ts @@ -0,0 +1,61 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { type FormEvent, type KeyboardEvent, useRef, useState } from "react"; + +type UseSearchOptions = { + onClose?: () => void; + clearOnSubmit?: boolean; +}; + +export function useSearch(options: UseSearchOptions = {}) { + const { onClose, clearOnSubmit = true } = options; + const router = useRouter(); + const [isOpen, setIsOpen] = useState(false); + const [query, setQuery] = useState(""); + const inputRef = useRef(null); + + const open = () => setIsOpen(true); + + const close = () => { + setIsOpen(false); + inputRef.current?.blur(); + onClose?.(); + }; + + const reset = () => { + setQuery(""); + close(); + }; + + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + const trimmedQuery = query.trim(); + if (trimmedQuery) { + router.push(`/search?q=${encodeURIComponent(trimmedQuery)}`); + if (clearOnSubmit) { + reset(); + } else { + close(); + } + } + }; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") { + close(); + } + }; + + return { + isOpen, + query, + setQuery, + inputRef, + open, + close, + reset, + handleSubmit, + handleKeyDown, + }; +} diff --git a/x_docs/DONE/search.md b/x_docs/DONE/search.md new file mode 100644 index 0000000..4acc7de --- /dev/null +++ b/x_docs/DONE/search.md @@ -0,0 +1,139 @@ +# Search Design Specification - STALE DOCUMENTATION / HISTORICAL RECORD ONLY + +Target: Stack Overflow-inspired search pattern adapted for DevFlow. Breakpoint: `md` (768px). + +**Reference screenshots:** `z-mobile.jpg`, `z-desktop.jpg` + +> **Scope:** This specification covers ONLY the search input and hints panel. Reference screenshots show Stack Overflow's full UI — ignore all elements except the search components (topbar search icon/input and hints dropdown). + +--- + +## Functional Requirements + +| Requirement | Details | +|-------------|---------| +| **Focus behaviour** | Dropdown opens on input focus, closes on blur/Escape | +| **Close behaviour** | Click outside OR Escape key closes search completely (2-state model) | +| **Form submission** | Enter key submits to `/search?q={query}` | +| **Operator insertion** | None — hints are display-only guidance | + +--- + +## Current Topbar Structure + +**Mobile (`< md`) — `mobile-topbar.tsx`:** + +``` +Logo | [SEARCH ICON - to add] | Hamburger (MobileNav) +``` + +**Desktop (`>= md`) — `desktop-topbar.tsx`:** + +``` +Searchbox (placeholder) | Sign in / Sign up +``` + +**Existing:** `components/search/searchbox.tsx` — placeholder component, already imported in desktop topbar. + +--- + +## Mobile Search + +**Topbar modification:** Add search icon button between Logo and Hamburger menu. + +**Behaviour (2-state model):** + +1. **Idle state:** Search icon only visible in topbar (between logo and hamburger) +2. **Active state (on click):** + - Search input appears **below topbar** at full width + - Search icon in topbar gets subtle highlight + - Topbar remains intact (logo, search icon, hamburger visible) + - After 0.5s delay: Static hints panel appears below input + - **Close:** Click outside OR Escape → returns to idle state + +**Hints Panel:** + +- Single column layout +- **Overlays page content** (positioned absolutely, does not push content down) +- Non-interactive guidance (not selectable) +- Footer: "Ask a question" button (primary style) → `/questions/ask` + +--- + +## Desktop Search (>= md) + +**Topbar:** Searchbox (always visible) | Sign in / Sign up + +**Behaviour (2-state model):** + +1. **Idle state:** Search input always visible inline in topbar (current `Searchbox` location) +2. **Active state (on focus):** + - Hints dropdown appears immediately below input (no delay) + - Input gets focus ring/highlight + - Topbar remains intact + - **Close:** Click outside OR Escape → hints close + +**Hints Dropdown:** + +- **Two-column layout** (left column: tag/user/phrase, right column: filters) +- **Overlays page content** (positioned absolutely, does not push content down) +- Non-interactive guidance (not selectable) +- Footer: "Ask a question" button (primary style) → `/questions/ask` +- **Excluded:** "Search help" link (visible in reference screenshot, not implementing) + +--- + +## Shared Hints Content + +| Syntax | Description | +|--------|-------------| +| `[tag]` | search within a tag | +| `user:1234` | search by author | +| `"words here"` | exact phrase | +| `answers:0` | unanswered questions | +| `score:3` | posts with a 3+ score | +| `is:question` | type of post | + +--- + +## Divergences from Stack Overflow + +**Excluded hints (not applicable to DevFlow):** + +- `collective:"Name"` — collective content (SO-specific feature) +- `isaccepted:yes` — search within status (not implemented) + +**Excluded UI elements:** + +- "Search help" link — not required + +**Behaviour changes:** + +- **Mobile close:** SO uses 3-state model (idle → input+hints → input-only). DevFlow uses simpler 2-state model (idle ↔ active) — click outside closes completely + +--- + +## Key Differences + +| Aspect | Mobile | Desktop | +|--------|--------|---------| +| Search visibility | Icon only (hidden input) | Always visible input | +| Input position | Below topbar (overlay) | Inline in topbar | +| Hints layout | Single column | Two columns | + +--- + +## Implementation Notes + +- **Component type:** Expandable search input with static hints popover +- **Not a combobox:** Hints are guidance text, not selectable options +- **Hints panel positioning:** Absolutely positioned overlay (not in document flow), requires appropriate z-index +- Search input and hints panel are a single-level component (not modal/nested) +- Topbar remains visible when search is active (mobile) +- Mobile requires adding search icon to `mobile-topbar.tsx` +- Desktop uses existing `Searchbox` component location + +**Styling:** + +- Use `font-mono` for operator syntax display +- Use `text-muted-foreground` for descriptions diff --git a/x_docs/my_notes/prompt-QA.md b/x_docs/my_notes/prompt-QA.md new file mode 100644 index 0000000..c977000 --- /dev/null +++ b/x_docs/my_notes/prompt-QA.md @@ -0,0 +1,198 @@ +# Code Quality Evaluation + +Evaluate all **staged files** (`git diff --cached --name-only`) for code quality, architecture, and best practices. + +## Current Baseline Status + +All checks pass as of evaluation start - no need to run these yourself: + +- **Lint & Typecheck** (`npm run check`): ✅ Passing +- **Unit Tests** (`npm run test:unit`): ✅ 44 tests passing +- **E2E Tests** (`npm run test:e2e`): ✅ 17 tests passing + +## Tech Stack Context + +- Next.js 16 (App Router, async params) +- React 19 (React Compiler enabled) +- Tailwind CSS v4 (new @import syntax, @theme inline) +- shadcn/ui (New York style, CSS variables) +- TypeScript strict mode +- British English + +## Baseline Context + +Always read `globals.css` first to understand the project's theming tokens and CSS architecture. + +## Evaluation Criteria + +Analyse staged files against these principles, grouped by category: + +### 1. Architecture + +- Component responsibility and separation of concerns +- Custom hooks extracting reusable logic +- Appropriate file/folder structure + +Note: + +- For current desktop search architecture, see inbetween tags `` in appendix +- (do not limit architecture consideration to this only, its provided as an aid) + +### 2. shadcn/ui Approach + +- Use shadcn MCP tools to compare components against upstream registry +- Find best-practice usage examples via `get_item_examples_from_registries` +- Use the `/shadcn-ui:shadcn-ui` skill for guidance +- Check for proper composition patterns (Slot, asChild, variants via CVA) + +### 3. Tailwind Best Practices + +- Utility-first (avoid unnecessary custom CSS) +- Mobile-first responsive design (base styles for mobile, md:/lg: for larger) +- Consistent use of design tokens (bg-card not arbitrary colours) + +### 4. Centralised Theming + +- CSS variables for colours, spacing, radii +- No hardcoded colours or magic numbers +- Theme-aware styling (proper dark: variants) +- Consistent token usage across components + +### 5. Code Quality + +- **DRY**: Extract repeated patterns (but not prematurely) +- **YAGNI**: No speculative abstractions +- **Elegance**: Code that's easy to read and understand +- **Pragmatism**: When principles conflict, favour clarity and practicality + +## Process + +1. Read `globals.css` for theming context +2. Get staged files: `git diff --cached --name-only` +3. Read all staged files thoroughly +4. Launch explorer agents to understand surrounding context +5. Use shadcn MCP tools to check registry patterns and find examples +6. Analyse against all criteria above +7. Write findings to `report.md` + +## Output Format + +Write a `report.md` file with this structure: + +```md +# Code Quality Report + +**Files evaluated**: [list of staged files] +**Date**: [timestamp] + +## Summary + +[1-2 sentence overall assessment] + +## Architecture + +[Issues or "No issues found"] + +## shadcn/ui Approach + +[Issues or "No issues found"] + +## Tailwind Best Practices + +[Issues or "No issues found"] + +## Centralised Theming + +[Issues or "No issues found"] + +## Code Quality (DRY, YAGNI, Elegance) + +[Issues or "No issues found"] +``` + +### Issue Format + +Each issue should follow this format: + +```md +### 🔴 [Short issue title] + +**File**: `path/to/file.tsx:42` +**Problem**: [Clear description of what's wrong] +**Why it matters**: [Why this is bad practice / the consequence of not fixing] +``` + +### Severity Indicators + +- 🔴 **High**: Significant impact on maintainability, consistency, or correctness +- 🟡 **Medium**: Suboptimal but functional; worth addressing +- 🟢 **Low**: Minor improvement; nice-to-have + +## Guiding Principle + +**Elegant pragmatism**: Favour code that is easy to understand and practically maintainable. When principles conflict (e.g., DRY suggests extraction but YAGNI suggests it's premature), choose the option that results in clearer, more readable code. Flag genuine tradeoffs for user decision. + +## Important + +- Do NOT include code snippets or proposed fixes +- Focus on explaining WHY something is a problem +- User will investigate each issue separately to brainstorm solutions + +## Appendix: Current Desktop Search Architecture + + + +## Design + +The desktop search bar is intentionally narrower than the main content area (`max-w-2xl` = 672px vs content's `max-w-5xl` = 1024px). This provides visual distinction so the popover doesn't blend into the page content. + +## Component Hierarchy + +``` +desktop-topbar.tsx (server) +└── DesktopSearch (client) ─── desktop-search.tsx + ├── useSearch hook ─────── hooks/use-search.ts + ├── SearchInput ────────── search-input.tsx (shared with mobile) + ├── SearchPopoverContent ─ search-popover-content.tsx (shared) + └── SearchHints ────────── search-hints.tsx (shared, variant="desktop") +``` + +## Width Control Points + +**1. Container in `desktop-topbar.tsx:9-12`:** + +```tsx +
+
{/* ← Controls search bar max width (672px) */} + +
+
+``` + +**2. Popover in `search-popover-content.tsx:17`:** + +```tsx +className="w-(--radix-popover-trigger-width) ..." {/* ← Matches input width exactly */} +``` + +The `--radix-popover-trigger-width` CSS variable is auto-set by Radix to match the PopoverAnchor element (the `
` in DesktopSearch). + +## Breakpoints + +| Breakpoint | Viewport | Behaviour | +|------------|----------|-----------| +| md | 768px+ | Desktop topbar appears | +| lg | 1024px+ | Padding increases (`lg:px-10`) | +| xl | 1280px+ | Right sidebar appears, padding increases (`xl:px-14`) | + +## Key Files + +- `components/navigation/desktop-topbar.tsx` — container width constraints +- `components/search/search-popover-content.tsx` — popover width +- `components/search/desktop-search.tsx` — form/anchor wrapper + +## Shared Components + +`SearchInput`, `SearchPopoverContent`, `SearchHints`, and `useSearch` are shared with `mobile-search.tsx`. Mobile uses full-width overlay, completely independent layout. + +