Unify design system with blue/indigo palette and new UI primitives#178
Unify design system with blue/indigo palette and new UI primitives#178
Conversation
Consolidates the app's palette, primitives, and page shells so every route shares the same calm, blue-400 anchored look defined in CLAUDE.md. Foundation - Add --color-primary, --shadow-elevated tokens and retune .anna-page-container / default-bg-gridient to a sky-50 -> blue-50 -> indigo-50 gradient (dark: slate-950 -> slate-900 -> blue-950). - New layout primitives: ui/surface, ui/page-shell, ui/page-header, ui/decor-blobs - server-safe building blocks reused across pages. - Modernize Card (glass surface, dark mode) and FieldInput (rounded-lg, blue-400 focus ring, responsive width); recolor loading spinner. Global shells - DashboardContainer drops the muddy bg-gray-400 overlay so the unified page gradient shows through; adds subtle decor blobs. - Settings layout moves to Surface with split sidebar/content. - Auth signin / phone layouts get a Surface hero with segmented tabs. Server pages - 404 / user-not-found / error / payment success + canceled / policy support move to anna-page-container + Surface + DecorBlobs; drop hardcoded bg-slate-900 and replace #045fab literals with blue-400. - Home, profile activity, webhooks, comments, upload, admin, clippings layout, pricing content & skeleton retuned to blue-400 -> indigo-500. - Comments page drops its conflicting min-h-screen wrapper. Shared components - Footer / NavigationBar / Button / Noun editor: recolor accents to blue-400/indigo-500; remove all remaining #045fab hex literals. - BookCover: drop empty CSS module, use pure Tailwind with blue-toned shadow and hover translate. Loading states - DashboardLoadingPage: spinner + consistent centered layout. - CenterPageLoading, home skeleton, pricing skeleton recolored to blue tinted placeholders. https://claude.ai/code/session_0126FKHoW1xBRsuz1gGyi97E
There was a problem hiding this comment.
Code Review
This pull request implements a comprehensive UI overhaul to unify the design system using new primitives like Surface, PageHeader, and DecorBlobs, while standardizing the color palette and layouts across the application. Feedback points out an unsafe non-null assertion in the admin panel and suggests further architectural improvements, including refactoring the legacy Card component to use the Surface primitive, consolidating duplicated authentication layouts, and making the Button component polymorphic to reduce style duplication.
| <div className="h-2 w-2 rounded-full bg-blue-400" /> | ||
| <span className="text-sm font-medium text-slate-600 dark:text-slate-300"> | ||
| {t('Showing books')} {offset + 1} -{' '} | ||
| {offset + (data!.adminDashboard.uncheckedBooks?.length || 0)} |
There was a problem hiding this comment.
The non-null assertion (!) on data is unsafe. If the query returns no data or fails silently, this will cause a runtime error. It's better to use optional chaining with a fallback value to ensure the page doesn't crash.
| {offset + (data!.adminDashboard.uncheckedBooks?.length || 0)} | |
| {offset + (data?.adminDashboard?.uncheckedBooks?.length || 0)} |
| @@ -1,5 +1,7 @@ | |||
| import type React from 'react' | |||
|
|
|||
| import { cn } from '@/lib/utils' | |||
There was a problem hiding this comment.
| <section | ||
| className={cn( | ||
| 'm-4 rounded-2xl border border-white/40 bg-white/70 p-6 shadow-sm backdrop-blur-xl transition-shadow hover:shadow-md dark:border-slate-800/40 dark:bg-slate-900/70', | ||
| glow && 'ring-1 ring-blue-400/30', | ||
| className | ||
| )} | ||
| onClick={onClick} | ||
| style={style} | ||
| data-glow={glow} | ||
| > | ||
| {children} | ||
| </section> |
There was a problem hiding this comment.
Refactor the Card component to use the Surface primitive. This centralizes the glass effect styling and ensures that this legacy component stays in sync with any future updates to the design system's canonical surface styles.
<Surface
as="section"
className={cn(
'm-4 p-6 transition-shadow hover:shadow-md',
glow && 'ring-1 ring-blue-400/30',
className
)}
onClick={onClick}
style={style}
data-glow={glow}
>
{children}
</Surface>
| <section className="anna-page-container relative flex min-h-screen items-center justify-center overflow-hidden px-4 py-10"> | ||
| <DecorBlobs /> | ||
| <Surface | ||
| variant="elevated" | ||
| className="with-slide-in relative z-10 w-full max-w-md p-8 md:p-10" | ||
| > | ||
| <div className="mb-6 flex flex-col items-center justify-center"> | ||
| <Image | ||
| src={logoLight} | ||
| alt="clippingkk logo" | ||
| className="dark:hidden" | ||
| width={80} | ||
| height={80} | ||
| /> | ||
| <Image | ||
| src={logoDark} | ||
| alt="clippingkk logo" | ||
| className="hidden dark:block" | ||
| width={80} | ||
| height={80} | ||
| /> | ||
| <h1 className="font-lato mt-3 bg-gradient-to-r from-blue-400 via-blue-500 to-indigo-500 bg-clip-text text-2xl font-semibold tracking-tight text-transparent"> | ||
| ClippingKK | ||
| </h1> | ||
| </div> | ||
|
|
||
| <nav | ||
| className="mb-6 grid grid-cols-2 gap-1 rounded-xl border border-slate-200/70 bg-slate-50/80 p-1 dark:border-slate-800/60 dark:bg-slate-900/60" | ||
| aria-label="Auth methods" | ||
| > | ||
| <Link | ||
| href="/auth/phone" | ||
| aria-current="page" | ||
| className="rounded-lg bg-blue-400 px-4 py-2 text-center text-sm font-medium text-white shadow-sm transition-colors hover:bg-blue-500 dark:bg-blue-400 dark:text-slate-950 dark:hover:bg-blue-300" | ||
| > | ||
| {t('app.auth.phone')} | ||
| </Link> | ||
| <Link | ||
| href="/auth/signin" | ||
| className="rounded-lg px-4 py-2 text-center text-sm font-medium text-slate-600 transition-colors hover:text-slate-900 dark:text-slate-300 dark:hover:text-white" | ||
| > | ||
| {t('app.auth.signin')} | ||
| </Link> | ||
| </nav> | ||
|
|
||
| <Link | ||
| href="/auth/signin" | ||
| className={ | ||
| 'flex px-8 py-4 text-lg transition-colors duration-200 hover:bg-indigo-400' | ||
| } | ||
| > | ||
| {t('app.auth.signin')} | ||
| </Link> | ||
| <div className="space-y-4">{children}</div> | ||
|
|
||
| <div className="relative my-6"> | ||
| <div className="absolute inset-0 flex items-center"> | ||
| <span className="w-full border-t border-slate-200/70 dark:border-slate-800/60" /> | ||
| </div> | ||
| <hr className="my-2" /> | ||
| {children} | ||
| <hr className="my-2" /> | ||
| <div className="flex items-center justify-center"> | ||
| <a | ||
| href={`https://github.com/login/oauth/authorize?client_id=${GithubClientID}&scope=user:email`} | ||
| onClick={onGithubClick} | ||
| title="github login" | ||
| > | ||
| <GithubLogo /> | ||
| </a> | ||
| <div className="relative flex justify-center"> | ||
| <span className="bg-white/70 px-3 text-xs tracking-wider text-slate-500 uppercase dark:bg-slate-900/70 dark:text-slate-400"> | ||
| {t('app.auth.or') || 'or'} | ||
| </span> | ||
| </div> | ||
| </> | ||
| </Card> | ||
| </div> | ||
|
|
||
| <div className="flex items-center justify-center"> | ||
| <a | ||
| href={`https://github.com/login/oauth/authorize?client_id=${GithubClientID}&scope=user:email`} | ||
| onClick={onGithubClick} | ||
| title="github login" | ||
| className="rounded-xl p-2 transition-colors hover:bg-slate-100 dark:hover:bg-slate-800" | ||
| > | ||
| <GithubLogo /> | ||
| </a> | ||
| </div> | ||
| </Surface> | ||
| </section> |
There was a problem hiding this comment.
There is significant code duplication between src/app/auth/phone/layout.tsx and src/app/auth/signin/layout.tsx. Both layouts share the same structure, logo, title, navigation tabs, and social login section. Consider extracting this shared shell into a reusable component (e.g., AuthLayout) to improve maintainability and reduce duplication, which is one of the key goals of this PR.
| <Link | ||
| href={`/dash/${myUidInt}/unchecked`} | ||
| className="group relative overflow-hidden rounded-md bg-gradient-to-r from-blue-500 to-purple-500 px-6 py-2 font-medium text-white shadow-md transition-all duration-300 hover:translate-y-[-2px] hover:shadow-lg" | ||
| className="inline-flex items-center rounded-xl bg-blue-400 px-5 py-2 text-sm font-medium text-white shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:bg-blue-500 hover:shadow-md dark:bg-blue-400 dark:text-slate-950 dark:hover:bg-blue-300" |
There was a problem hiding this comment.
This primary button style is manually duplicated in many places across the application (Admin page, 404 page, Error page, Payment pages, etc.). To ensure consistency and reduce maintenance overhead, consider making the Button component polymorphic (using an as prop) so it can be used with the Next.js Link component while sharing the same visual styles and gradient logic.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fff1aec520
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| </a> | ||
| <div className="relative flex justify-center"> | ||
| <span className="bg-white/70 px-3 text-xs tracking-wider text-slate-500 uppercase dark:bg-slate-900/70 dark:text-slate-400"> | ||
| {t('app.auth.or') || 'or'} |
There was a problem hiding this comment.
Add missing
app.auth.or locale entry
The new separator label uses t('app.auth.or') || 'or', but no app.auth.or key was added in locale files, so i18next will return the literal key string (truthy) and the 'or' fallback never applies. This makes auth pages render app.auth.or to end users (same pattern appears in src/app/auth/signin/layout.tsx), so please add the key in translations or pass a defaultValue to t(...) instead of relying on ||.
Useful? React with 👍 / 👎.
PR Review: Unified Design System (blue/indigo palette + UI primitives)Overall this is a solid, well-motivated refactor. The four new primitives ( Bugs / CorrectnessMissing translation key Both auth layouts use
Design System ConsistencyRepeated CTA class string instead of The // Appears 10+ times across payment, not-found, error, admin pages:
className="inline-flex items-center gap-2 rounded-xl bg-blue-400 px-6 py-3 font-medium text-white shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:bg-blue-500 hover:shadow-md dark:bg-blue-400 dark:text-slate-950 dark:hover:bg-blue-300"This works but defeats the purpose of a centralized Defined CSS variables go unused
DuplicationAuth layouts are nearly identical
Minor Issues
// surface.tsx
const Component = (as ?? 'div') as React.ElementTypeThe cast to
Skeleton gradient is visually complex for a loading state // skeleton.tsx
className="... animate-pulse rounded-2xl border border-white/40 bg-gradient-to-br from-blue-400/10 via-slate-200/60 to-indigo-400/10 backdrop-blur-xl ..."The Positive Notes
Summary: Address the missing |
| } | ||
| > | ||
| <div className="dark:bg-opacity-80 bg-opacity-60 flex min-h-screen w-full flex-col bg-gray-400 backdrop-blur-xl dark:bg-gray-900"> | ||
| <section className="anna-page-container relative flex min-h-screen w-full flex-col overflow-hidden"> |
There was a problem hiding this comment.
🔴 overflow-hidden on DashboardContainer breaks sticky navigation bar
Adding overflow-hidden to the <section> in DashboardContainer breaks the position: sticky behavior of the <nav> element inside NavigationBar (src/components/navigation-bar/navigation-bar.tsx:63). Per CSS spec, when an ancestor has overflow set to anything other than visible, it becomes the nearest scroll container for sticky positioning. Since the <section> stretches to fit content and never actually scrolls, the nav will never "stick" — it scrolls away with the page. This affects every route that uses DashboardContainer: all /dash/[userid]/* pages, /pricing, /payment/*, and /policy/support. The fix is to replace overflow-hidden with overflow-clip (Tailwind's overflow-clip), which clips the decorative blobs without creating a scroll container.
| <section className="anna-page-container relative flex min-h-screen w-full flex-col overflow-hidden"> | |
| <section className="anna-page-container relative flex min-h-screen w-full flex-col overflow-clip"> |
Was this helpful? React with 👍 or 👎 to provide feedback.
| <div className="mx-auto mb-6 inline-flex h-16 w-16 items-center justify-center rounded-2xl bg-rose-400/10 ring-1 ring-rose-400/20 dark:bg-rose-400/15"> | ||
| <AlertTriangle className="h-8 w-8 text-rose-500 dark:text-rose-300" /> | ||
| </div> |
There was a problem hiding this comment.
🟡 inline-flex with mx-auto prevents icon centering on error page
The error icon container uses mx-auto mb-6 inline-flex h-16 w-16 ... but mx-auto has no centering effect on inline-level elements — auto margins on inline boxes compute to 0 per CSS spec. Unlike all other similar pages in this PR (e.g. src/app/not-found.tsx:24, src/app/payment/canceled/content.tsx:6) which wrap icons in a flex flex-col items-center parent, the error page's icon sits directly inside the <Surface> which is a plain <div> in normal block flow. The icon will render left-aligned instead of centered, breaking the visual symmetry with the centered heading and text above/below it.
| <div className="mx-auto mb-6 inline-flex h-16 w-16 items-center justify-center rounded-2xl bg-rose-400/10 ring-1 ring-rose-400/20 dark:bg-rose-400/15"> | |
| <AlertTriangle className="h-8 w-8 text-rose-500 dark:text-rose-300" /> | |
| </div> | |
| <div className="mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-2xl bg-rose-400/10 ring-1 ring-rose-400/20 dark:bg-rose-400/15"> | |
| <AlertTriangle className="h-8 w-8 text-rose-500 dark:text-rose-300" /> | |
| </div> |
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary
This PR introduces a unified design system across the application by establishing a canonical blue-400 → indigo-500 gradient palette, creating reusable UI primitives (
Surface,DecorBlobs,PageHeader,PageShell), and refactoring existing pages to use these new components. This ensures visual consistency and reduces duplication across auth, error, payment, policy, and dashboard pages.Key Changes
New UI Primitives
Surface— Glass-morphism card wrapper with four variants (default,muted,elevated,plain) replacing ad-hocCardand inline glass stylesDecorBlobs— Reusable decorative gradient blob background with tone variants (primary,danger,success)PageHeader— Unified page/section title component with optional eyebrow, icon, description, and action slots; enforces the blue-400 → indigo-500 gradientPageShell— Server-side page wrapper providing consistent padding, max-width, and slide-in animationColor Palette Unification
/auth/phone,/auth/signin)tailwind.csswith CSS variables for the primary palettePage Refactors
CardwithSurface, addedDecorBlobs, introduced navigation tabs with active state stylingDecorBlobs, wrapped content inSurfaceDecorBlobsandSurfacewrappers, improved icon and CTA stylingComponent Updates
FieldInput— Modernized styling with responsive layout and updated color schemeButton— Adjusted primary variant to use blue-400 → indigo-500 gradientCard— Kept for backward compatibility; now visually matchesSurfacePageHeadercomponent for consistent title stylingStyle Cleanup
book-cover.module.css)anna-page-containerclasspointer-events-noneto decorative elements for better UXMinor Fixes
aria-labelandaria-currentattributes on navigationImplementation Details
cn()utility for class composition and support dark modeDecorBlobssupports bothabsoluteandfixedpositioning for flexibilitySurfaceuses polymorphic component pattern (asprop) for semantic HTMLblur-3xland opacity values tuned for the new palettewith-slide-inanimation class for consistent entrancehttps://claude.ai/code/session_0126FKHoW1xBRsuz1gGyi97E