Skip to content

Commit 8fc66d4

Browse files
committed
fix: improve contrast and ui polish
1 parent 4365233 commit 8fc66d4

18 files changed

Lines changed: 761 additions & 964 deletions

apps/web/app/create/page.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,24 @@
11
import { SoulBuilderContent } from '@/components/create/soul-builder-content'
22
import { createAuthPageMetadata } from '@/lib/seo'
3+
import { Loader2 } from 'lucide-react'
4+
import { Suspense } from 'react'
35

46
export const metadata = createAuthPageMetadata(
57
'Create Soul',
68
'Generate a first-draft SOUL.md, refine it, and continue to the upload flow.'
79
)
810

911
export default function CreatePage() {
10-
return <SoulBuilderContent />
12+
return (
13+
<Suspense
14+
fallback={
15+
<div className="flex flex-1 items-center justify-center flex-col gap-3">
16+
<h1 className="text-xl font-medium text-text">Create a soul</h1>
17+
<Loader2 className="w-5 h-5 animate-spin text-text-secondary" />
18+
</div>
19+
}
20+
>
21+
<SoulBuilderContent />
22+
</Suspense>
23+
)
1124
}

apps/web/app/globals.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@
4242

4343
/* Text */
4444
--color-text: #ededed;
45-
--color-text-secondary: #878787;
46-
--color-text-muted: #525252;
45+
--color-text-secondary: #9a9a9a;
46+
--color-text-muted: #6a6a6a;
4747

4848
/* Minimal accent - white for active states */
4949
--color-accent: #ededed;

apps/web/components/breadcrumb.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,22 +48,22 @@ export function Breadcrumb({ items, className = '' }: BreadcrumbProps) {
4848
return (
4949
<>
5050
{/* JSON-LD structured data for SEO */}
51-
<script
52-
type="application/ld+json"
53-
// biome-ignore lint/security/noDangerouslySetInnerHtml: JSON-LD is safe
54-
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
55-
/>
51+
<script type="application/ld+json">{JSON.stringify(jsonLd)}</script>
5652

5753
{/* Visible breadcrumb navigation */}
5854
<nav aria-label="Breadcrumb" className={className}>
59-
<ol className="flex items-center gap-2 text-xs font-mono">
55+
<ol className="flex items-center gap-2 text-[13px] font-mono">
6056
{fullItems.map((item, index) => {
6157
const isLast = index === fullItems.length - 1
6258
const isActive = item.href === pathname
6359

6460
return (
6561
<li key={item.href || item.name} className="flex items-center gap-2">
66-
{index > 0 && <span className="text-text-muted select-none">/</span>}
62+
{index > 0 && (
63+
<span className="select-none text-text-secondary" aria-hidden="true">
64+
/
65+
</span>
66+
)}
6767
{item.href && !isLast ? (
6868
<Link
6969
href={item.href as Route}
@@ -75,7 +75,10 @@ export function Breadcrumb({ items, className = '' }: BreadcrumbProps) {
7575
{item.name}
7676
</Link>
7777
) : (
78-
<span className="text-text-muted" aria-current={isLast ? 'page' : undefined}>
78+
<span
79+
className="font-medium text-text-secondary"
80+
aria-current={isLast ? 'page' : undefined}
81+
>
7982
{item.name}
8083
</span>
8184
)}

apps/web/components/home/home-content.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export function HomeContent({
8585
</div>
8686

8787
{/* Stats row */}
88-
<div className="flex flex-wrap justify-center items-center gap-4 text-xs text-text-muted font-mono mb-8">
88+
<div className="mb-8 flex flex-wrap items-center justify-center gap-4 font-mono text-xs text-text-secondary">
8989
<span>{totalSouls.toLocaleString()} souls</span>
9090
<span className="w-px h-3 bg-border" aria-hidden="true" />
9191
<span>{categories.length} categories</span>
@@ -246,7 +246,7 @@ export function HomeContent({
246246
className="group flex items-center justify-between gap-4 rounded-md border border-border bg-surface px-4 py-3 transition-all duration-200 hover:border-text-muted"
247247
>
248248
<CategoryBadge category={category} size="md" variant="minimal" />
249-
<span className="text-xs text-text-muted font-mono">
249+
<span className="text-xs text-text-secondary font-mono">
250250
{category.soulCount?.toLocaleString()}
251251
</span>
252252
</Link>

apps/web/components/layout/__tests__/header.test.tsx

Lines changed: 81 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,24 @@
1-
import { render, screen } from '@testing-library/react'
1+
import { fireEvent, render, screen } from '@testing-library/react'
22
import * as React from 'react'
33
import { describe, expect, it, vi } from 'vitest'
44
import { Header } from '../header'
55

6+
const mockUseAuthStatus = vi.fn<
7+
() => {
8+
me: { handle?: string; displayName?: string; email?: string } | null
9+
isLoading: boolean
10+
isAuthenticated: boolean
11+
signIn: ReturnType<typeof vi.fn>
12+
signOut: ReturnType<typeof vi.fn>
13+
}
14+
>(() => ({
15+
me: null,
16+
isLoading: false,
17+
isAuthenticated: false,
18+
signIn: vi.fn(),
19+
signOut: vi.fn(),
20+
}))
21+
622
// Mock next/link
723
vi.mock('next/link', () => ({
824
default: ({ children, href, ...props }: { children: React.ReactNode; href: string }) =>
@@ -17,13 +33,7 @@ vi.mock('next/navigation', () => ({
1733

1834
// Mock useAuthStatus hook to simulate logged-out state
1935
vi.mock('@/hooks/use-auth-status', () => ({
20-
useAuthStatus: () => ({
21-
me: null,
22-
isLoading: false,
23-
isAuthenticated: false,
24-
signIn: vi.fn(),
25-
signOut: vi.fn(),
26-
}),
36+
useAuthStatus: () => mockUseAuthStatus(),
2737
}))
2838

2939
// Mock GithubStars component
@@ -37,13 +47,48 @@ vi.mock('@/components/search/search-autocomplete', () => ({
3747
}))
3848

3949
describe('Header', () => {
50+
it('shows generate and submit inside the authenticated user menu', () => {
51+
mockUseAuthStatus.mockReturnValue({
52+
me: {
53+
handle: 'david',
54+
displayName: 'David',
55+
email: 'david@example.com',
56+
},
57+
isLoading: false,
58+
isAuthenticated: true,
59+
signIn: vi.fn(),
60+
signOut: vi.fn(),
61+
})
62+
63+
render(<Header />)
64+
65+
fireEvent.pointerDown(screen.getByRole('button', { name: 'User menu' }))
66+
67+
expect(screen.getByRole('menuitem', { name: 'Generate' })).toHaveAttribute('href', '/create')
68+
expect(screen.getByRole('menuitem', { name: 'Submit' })).toHaveAttribute('href', '/upload')
69+
})
70+
4071
it('should render the logo', () => {
72+
mockUseAuthStatus.mockReturnValue({
73+
me: null,
74+
isLoading: false,
75+
isAuthenticated: false,
76+
signIn: vi.fn(),
77+
signOut: vi.fn(),
78+
})
4179
render(<Header />)
4280

4381
expect(screen.getByText('souls.directory')).toBeInTheDocument()
4482
})
4583

4684
it('should render navigation links', () => {
85+
mockUseAuthStatus.mockReturnValue({
86+
me: null,
87+
isLoading: false,
88+
isAuthenticated: false,
89+
signIn: vi.fn(),
90+
signOut: vi.fn(),
91+
})
4792
render(<Header />)
4893

4994
// Use exact name "Souls" to avoid matching the logo link "souls.directory"
@@ -53,6 +98,13 @@ describe('Header', () => {
5398
})
5499

55100
it('should render Sign Up button when logged out', () => {
101+
mockUseAuthStatus.mockReturnValue({
102+
me: null,
103+
isLoading: false,
104+
isAuthenticated: false,
105+
signIn: vi.fn(),
106+
signOut: vi.fn(),
107+
})
56108
render(<Header />)
57109

58110
// Sign Up button should be visible when user is not authenticated
@@ -61,13 +113,27 @@ describe('Header', () => {
61113
})
62114

63115
it('should have correct href for logo', () => {
116+
mockUseAuthStatus.mockReturnValue({
117+
me: null,
118+
isLoading: false,
119+
isAuthenticated: false,
120+
signIn: vi.fn(),
121+
signOut: vi.fn(),
122+
})
64123
render(<Header />)
65124

66125
const logo = screen.getByRole('link', { name: /souls\.directory/i })
67126
expect(logo).toHaveAttribute('href', '/')
68127
})
69128

70129
it('should have correct hrefs for navigation links', () => {
130+
mockUseAuthStatus.mockReturnValue({
131+
me: null,
132+
isLoading: false,
133+
isAuthenticated: false,
134+
signIn: vi.fn(),
135+
signOut: vi.fn(),
136+
})
71137
render(<Header />)
72138

73139
expect(screen.getByRole('link', { name: 'Souls' })).toHaveAttribute('href', '/souls')
@@ -76,6 +142,13 @@ describe('Header', () => {
76142
})
77143

78144
it('should apply sticky positioning', () => {
145+
mockUseAuthStatus.mockReturnValue({
146+
me: null,
147+
isLoading: false,
148+
isAuthenticated: false,
149+
signIn: vi.fn(),
150+
signOut: vi.fn(),
151+
})
79152
render(<Header />)
80153

81154
const header = screen.getByRole('banner')

apps/web/components/layout/breadcrumb.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,7 @@ export function Breadcrumb({ items, className = '' }: BreadcrumbProps) {
4848
return (
4949
<>
5050
{/* JSON-LD structured data for SEO */}
51-
<script
52-
type="application/ld+json"
53-
// biome-ignore lint/security/noDangerouslySetInnerHtml: JSON-LD is safe
54-
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
55-
/>
51+
<script type="application/ld+json">{JSON.stringify(jsonLd)}</script>
5652

5753
{/* Visible breadcrumb navigation */}
5854
<nav aria-label="Breadcrumb" className={className}>

apps/web/components/layout/header.tsx

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ import {
2323
DropdownMenuTrigger,
2424
} from '@/components/ui/dropdown-menu'
2525
import { useAuthStatus } from '@/hooks/use-auth-status'
26-
import { LayoutDashboard, Loader2, LogOut, Plus, Settings } from 'lucide-react'
26+
import { LayoutDashboard, Loader2, LogOut, Plus, Settings, Sparkles } from 'lucide-react'
27+
import Image from 'next/image'
2728
import Link from 'next/link'
2829
import { usePathname, useRouter } from 'next/navigation'
2930
import { useEffect, useRef, useState } from 'react'
@@ -57,12 +58,15 @@ function UserAvatar({
5758
size?: 'sm' | 'md'
5859
}) {
5960
const sizeClasses = size === 'sm' ? 'w-7 h-7 text-xs' : 'w-9 h-9 text-sm'
61+
const imageSize = size === 'sm' ? 28 : 36
6062

6163
if (user?.image) {
6264
return (
63-
<img
65+
<Image
6466
src={user.image}
6567
alt={user.displayName || user.handle || 'User avatar'}
68+
width={imageSize}
69+
height={imageSize}
6670
className={`${sizeClasses} rounded-full object-cover border border-border`}
6771
/>
6872
)
@@ -101,15 +105,17 @@ export function Header() {
101105
const handleScroll = () => {
102106
const scrollY = window.scrollY
103107
const delta = scrollY - lastScrollY.current
108+
let nextVisible = headerVisible
104109

105110
if (scrollY < SCROLL_TOP_THRESHOLD) {
106-
setHeaderVisible(true)
111+
nextVisible = true
107112
} else if (delta > SCROLL_DELTA_THRESHOLD) {
108-
setHeaderVisible(false)
113+
nextVisible = false
109114
} else if (delta < -SCROLL_DELTA_THRESHOLD) {
110-
setHeaderVisible(true)
115+
nextVisible = true
111116
}
112117

118+
setHeaderVisible(nextVisible)
113119
lastScrollY.current = scrollY
114120
}
115121

@@ -126,7 +132,7 @@ export function Header() {
126132
window.removeEventListener('scroll', onScroll)
127133
if (rafId.current !== null) cancelAnimationFrame(rafId.current)
128134
}
129-
}, [])
135+
}, [headerVisible])
130136

131137
const handleSignOut = async () => {
132138
await signOut()
@@ -247,6 +253,24 @@ export function Header() {
247253
</DropdownMenuLabel>
248254
)}
249255
<DropdownMenuSeparator className="bg-border" />
256+
<DropdownMenuItem asChild>
257+
<Link
258+
href={ROUTES.create}
259+
className="cursor-pointer text-text-secondary hover:text-text"
260+
>
261+
<Sparkles className="mr-2 h-4 w-4" />
262+
Generate
263+
</Link>
264+
</DropdownMenuItem>
265+
<DropdownMenuItem asChild>
266+
<Link
267+
href={ROUTES.upload}
268+
className="cursor-pointer text-text-secondary hover:text-text"
269+
>
270+
<Plus className="mr-2 h-4 w-4" />
271+
Submit
272+
</Link>
273+
</DropdownMenuItem>
250274
<DropdownMenuItem asChild>
251275
<Link
252276
href={ROUTES.dashboard}

0 commit comments

Comments
 (0)