Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions app/(app)/search/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
type SearchPageProps = {
searchParams: Promise<{ q?: string }>;
};

export default async function SearchPage({ searchParams }: SearchPageProps) {
const { q } = await searchParams;

return (
<div className="space-y-6">
<h1 className="text-heading-lg">Search Results</h1>
{q ? (
<p className="text-muted-foreground">
Showing results for: <strong className="text-foreground">{q}</strong>
</p>
) : (
<p className="text-muted-foreground">Enter a search query to begin.</p>
)}

{/* TODO: Implement search results */}
<div className="rounded-lg border border-dashed p-8 text-center text-muted-foreground">
Do Search on mock data (+TDD) → then Convex.
</div>
</div>
);
}
90 changes: 42 additions & 48 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);

/* Custom */
/* Custom: Overlay */
--color-overlay: var(--overlay);
}

Expand All @@ -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 */
Expand All @@ -102,55 +102,52 @@
--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%,
var(--primary-gradient-to) 99.54%
);

/* 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%);
Expand All @@ -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 {
Expand Down Expand Up @@ -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;
}
}
8 changes: 4 additions & 4 deletions components/navigation/desktop-topbar.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<header className="sticky top-0 z-40 hidden h-(--top-bar-height) items-center bg-background md:flex">
{/* 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) */}
<div className="flex flex-1 items-center md:px-8 lg:px-10 xl:px-14">
<div className="mx-auto w-full max-w-5xl">
<Searchbox />
<div className="mx-auto w-full max-w-2xl">
<DesktopSearch />
</div>
</div>

Expand Down
6 changes: 5 additions & 1 deletion components/navigation/mobile-topbar.tsx
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -10,7 +11,10 @@ export function MobileTopBar() {
<img src="/images/site-logo.svg" className="size-8" />
</Link>

<MobileNav />
<div className="flex items-center">
<MobileSearch />
<MobileNav />
</div>
</header>
);
}
40 changes: 40 additions & 0 deletions components/search/desktop-search.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Popover open={isOpen} onOpenChange={(open) => !open && close()}>
<PopoverAnchor asChild>
<form data-slot="searchbox" onSubmit={handleSubmit} className="w-full">
<SearchInput
ref={inputRef}
value={query}
onChange={setQuery}
onFocus={open}
onKeyDown={handleKeyDown}
highlighted={isOpen}
/>
</form>
</PopoverAnchor>
<SearchPopoverContent>
<SearchHints variant="desktop" onClose={close} />
</SearchPopoverContent>
</Popover>
);
}
105 changes: 105 additions & 0 deletions components/search/mobile-search.tsx
Original file line number Diff line number Diff line change
@@ -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 */}
<Button
variant="ghost"
size="icon-lg"
onClick={open}
className={cn(
"text-foreground md:hidden",
isOpen && "bg-accent text-accent-foreground",
)}
aria-label="Search"
>
<Search className="size-6" />
</Button>

{/* Overlay and search panel (only when active) */}
{isOpen && (
<>
{/* Backdrop overlay - click to close */}
<button
type="button"
tabIndex={-1}
data-slot="mobile-search-overlay"
className="animate-in fade-in-0 fixed inset-0 z-50 cursor-default bg-overlay/50 duration-300 md:hidden"
onClick={reset}
aria-label="Close search"
/>

{/* Search input bar - separate card floating below topbar */}
<div
data-slot="mobile-search-input-bar"
className="animate-in slide-in-from-top-2 fade-in-0 fixed left-0 right-0 top-(--top-bar-height) z-50 border-b bg-card px-4 py-3 shadow-sm duration-300 md:hidden"
Copy link

@coderabbitai coderabbitai bot Jan 7, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix invalid Tailwind CSS syntax for CSS custom property.

The syntax top-(--top-bar-height) is invalid. Tailwind requires square brackets for arbitrary values with CSS custom properties.

🔎 Proposed fix
-            className="animate-in slide-in-from-top-2 fade-in-0 fixed left-0 right-0 top-(--top-bar-height) z-50 border-b bg-card px-4 py-3 shadow-sm duration-300 md:hidden"
+            className="animate-in slide-in-from-top-2 fade-in-0 fixed left-0 right-0 top-[var(--top-bar-height)] z-50 border-b bg-card px-4 py-3 shadow-sm duration-300 md:hidden"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
className="animate-in slide-in-from-top-2 fade-in-0 fixed left-0 right-0 top-(--top-bar-height) z-50 border-b bg-card px-4 py-3 shadow-sm duration-300 md:hidden"
className="animate-in slide-in-from-top-2 fade-in-0 fixed left-0 right-0 top-[var(--top-bar-height)] z-50 border-b bg-card px-4 py-3 shadow-sm duration-300 md:hidden"
🤖 Prompt for AI Agents
In @components/search/mobile-search.tsx at line 80, The className contains
invalid Tailwind syntax `top-(--top-bar-height)` in the mobile search component;
update the arbitrary value to use square-bracket syntax with the CSS custom
property, e.g., replace that token with the Tailwind arbitrary form using the
CSS variable (use top-[var(--top-bar-height)]) inside the same className string
so Tailwind recognizes the value.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Hi @coderabbitai,

I'm skipping this comment because I'm using Tailwind v4.1, and the syntax top-(--top-bar-height) is actually valid.

In Tailwind v4, the parentheses syntax is a shorthand that automatically wraps the CSS variable in var():

<!-- v4 shorthand (what I'm using) -->
<div class="top-(--my-variable)">

<!-- Equivalent traditional syntax -->
<div class="top-[var(--my-variable)]">

From the Tailwind docs:

If you're referencing a CSS variable as an arbitrary value, you can use the custom property syntax... This is just a shorthand for fill-[var(--my-brand-color)] that adds the var() function for you automatically.

No changes needed here! 🎉

Copy link

Choose a reason for hiding this comment

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

@michellepace, you're absolutely right, and I apologize for the incorrect flag!

The top-(--top-bar-height) syntax is indeed valid in Tailwind v4. I should have caught this, especially since I have a learning that confirms this project uses Tailwind v4's shorthand syntax with parentheses for CSS variable references (like bg-(image:--gradient-primary)). The parentheses syntax automatically wraps the variable in var(), making it equivalent to top-[var(--top-bar-height)].

Thank you for the clarification and the documentation reference! The code is correct as written. 🎉

(・ω・) 📚


🧠 Learnings used
Learnt from: michellepace
Repo: michellepace/devflow PR: 18
File: lib/utils.ts:17-19
Timestamp: 2025-12-25T15:46:14.812Z
Learning: In Tailwind CSS v4, the shorthand syntax `bg-(image:--gradient-primary)` with parentheses is valid and equivalent to the traditional bracket syntax `bg-[image:var(--gradient-primary)]`. The project michellepace/devflow has standardized on using the v4 shorthand syntax with parentheses for CSS variable references.

Learnt from: CR
Repo: michellepace/devflow PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T22:41:43.692Z
Learning: Applies to **/*.css : Tailwind CSS v4: Use import "tailwindcss" syntax instead of tailwind directives

>
<Popover open={showHints} onOpenChange={setShowHints}>
<PopoverAnchor asChild>
<form data-slot="mobile-search-form" onSubmit={handleSubmit}>
<SearchInput
ref={inputRef}
value={query}
onChange={setQuery}
onKeyDown={handleKeyDown}
highlighted={isOpen}
/>
</form>
</PopoverAnchor>

{/* Hints panel - separate floating card below input */}
<SearchPopoverContent sideOffset={12}>
<SearchHints variant="mobile" onClose={reset} />
</SearchPopoverContent>
</Popover>
</div>
</>
)}
</>
);
}
10 changes: 10 additions & 0 deletions components/search/search-hints-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const SEARCH_HINTS = [
{ syntax: "[tag]", description: "search within a tag" },
{ syntax: "user:1234", description: "search by author" },
{ syntax: '"words here"', description: "exact phrase" },
{ syntax: "answers:0", description: "unanswered questions" },
{ syntax: "score:3", description: "posts with a 3+ score" },
{ syntax: "is:question", description: "type of post" },
] as const;

export type SearchHint = (typeof SEARCH_HINTS)[number];
Loading
Loading