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
9 changes: 5 additions & 4 deletions .ai/frontend-state.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"agent": "Claude Code",
"area": "frontend",
"currentPhase": "F7 API integration (in progress)",
"lastVerifiedTask": "TMS-F7-006b",
"lastVerifiedTask": "TMS-F0-013",
"nextRecommendedTask": "F7 on claude/f7-api-integration (PR #23). DONE: TMS-F7-001..004 (http foundation, per-domain data source, artworks+collections, nullable price), F7-005 (Studio on approved placements), F7-006a (canonical lineId + cart client), F7-006b (cart UI on the server cart: server totals, issue lines kept + reasons, hasIssues blocks checkout, promotion 422 vs 200-with-null). NEXT: TMS-F7-008 customer auth cutover (/api/v1/auth/*, opaque tms_session cookie) — it unblocks TMS-F7-007 saved designs (client + tests already done in lib/designs-api.ts) and makes the guest-cart merge real. BLOCKERS: (1) no live API locally — Postgres has only B1 identity tables and migrating it would disturb the parallel backend session, so everything is stub-verified; (2) TMS-FBR-020 — a cart line is pure ids and cannot be rendered from its own response, so lib/cart-view.ts joins labels from /artworks + /garments as an interim.",
"completedTasks": [
"TMS-F0-001",
Expand All @@ -17,6 +17,7 @@
"TMS-F0-010",
"TMS-F0-011",
"TMS-F0-012",
"TMS-F0-013",
"TMS-F1-001",
"TMS-F1-002",
"TMS-F1-003",
Expand Down Expand Up @@ -64,7 +65,7 @@
],
"buildStatus": "passing",
"testStatus": "passing",
"visualRegressionStatus": "baselines-committed (4 viewports, homepage; Windows-rendered, not in CI)",
"visualRegressionStatus": "TMS-F0-013 logo surfaces verified at 375px and 1440px (storefront header/footer/menu, admin login/shell); prior homepage baselines remain committed",
"mockDependencies": [
"storefront:home:featuredArtworks (TMS-FBR-001)",
"storefront:/artworks listing + detail (TMS-FBR-001)",
Expand Down Expand Up @@ -96,7 +97,7 @@
"storefront:studio guide (/studio-guide) — deterministic mock assistant (lib/studio-guide.ts), no LLM/live tools; guardrails deflect stock/price/delivery; order-status = tool_error + retry; real assistant endpoint + tool results are backend (TMS-FBR-009)",
"storefront:loyalty & referrals (/account/loyalty) — illustrative email-derived points/tier + rewards + referral code (lib/loyalty.ts); redeem/share are preview-only; real ledger/earn/redeem + referral attribution are backend (TMS-FBR-008)"
],
"branch": "claude/f5-post-merge",
"branch": "codex/fatu-logo-system",
"buildStatusNote": "F5 (growth & AI) COMPLETE — all of TMS-F5-001…010 Verified (pnpm check green). Storefront: 001 drops, 002 waitlist, 003 pre-order/made-to-order, 004 reviews, 005 community gallery, 006 artwork passport, 007 shoppable stories, 008 studio guide, 010 loyalty & referrals. Admin: 009 brand storyteller. F0–F4 Verified AND MERGED TO MAIN (2026-07-15). The F0→F4 PR stack (#4→#8) was merged bottom-up with merge commits (main HEAD e919e7e; merge commits 9599d35/190d8c7/d0201a1/f0de066/e919e7e) and all five phase branches deleted; pnpm check re-run green on the integrated main (13/13 build tasks, all unit tests, format/lint/typecheck/db:validate). Recovery note: PR #5 auto-closed when gh's --delete-branch removed its base (f0) before the auto-retarget fired; recovered by recreating the f0 branch, reopening #5, retargeting to main — thereafter each next PR was retargeted to main BEFORE merge and branches were deleted only as final cleanup. Only open item is defect TMS-F1-DEF-001 (soft 404, SEO-only). Admin Vitest 125 tests (was 116 — +9 storyteller); storefront 195 (was 89 — +24 drops, +6 waitlist, +10 fulfilment, +11 passport, +11 stories, +13 reviews, +12 community, +8 studio-guide, +11 loyalty). Note: pnpm audit 410 outage since F3-004 (this is the only failing CI 'quality' check — a retired npm endpoint, not a code issue); no new deps added.",
"lastUpdated": "2026-07-16T03:30:00Z"
"lastUpdated": "2026-07-20T23:40:00+01:00"
}
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ All notable changes to Tai Manic Studios are documented here.

The project uses a four-part release version: `major.minor.patch.micro`.

## [0.1.1.0] - 2026-07-21

### Added

- Added the supplied F.A.T.U brand mark across the storefront and admin experience, including responsive navigation, authentication, and footer placements.
- Added branded browser icons, an Apple touch icon, social sharing artwork, and an installable storefront manifest.

### Changed

- Replaced text-only F.A.T.U wordmarks with reusable, optimized logo components and documented the verified frontend handoff.

## [0.1.0.0] - 2026-07-14

### Added
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.1.0.0
0.1.1.0
Binary file added apps/admin/app/apple-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/admin/app/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions apps/admin/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,13 @@ const sans = IBM_Plex_Sans({
});

export const metadata: Metadata = {
applicationName: 'F.A.T.U Admin',
title: { default: 'Admin · From Africa To You', template: '%s · F.A.T.U Admin' },
description: 'Operational console for From Africa To You.',
icons: {
icon: [{ url: '/icon.png', type: 'image/png', sizes: '512x512' }],
apple: [{ url: '/apple-icon.png', type: 'image/png', sizes: '180x180' }],
},
robots: { index: false, follow: false, nocache: true },
};

Expand Down
8 changes: 7 additions & 1 deletion apps/admin/components/admin-login-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { type AuthErrors, validateLogin } from '@/lib/admin-auth';
import { useAdminAuth } from './admin-auth-provider';
import { BrandLogo } from './brand-logo';

const inputClass =
'h-11 w-full rounded-md border bg-surface px-3 text-sm text-ink outline-none placeholder:text-muted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-focus-ring)]';
Expand Down Expand Up @@ -33,7 +34,12 @@ export function AdminLoginForm() {
return (
<div className="w-full max-w-sm">
<div className="mb-8 text-center">
<span className="font-display text-lg font-semibold tracking-tight text-ink">F.A.T.U</span>
<BrandLogo
alt="F.A.T.U — From Africa To You"
className="mx-auto size-20"
priority
sizes="80px"
/>
<p className="mt-1 font-display text-[0.65rem] font-semibold uppercase tracking-[0.16em] text-muted">
Admin
</p>
Expand Down
33 changes: 15 additions & 18 deletions apps/admin/components/admin-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import Link from 'next/link';
import { usePathname, useRouter } from 'next/navigation';
import { type MouseEvent, useCallback, useEffect, useRef } from 'react';
import { useAdminAuth } from './admin-auth-provider';
import { BrandLogo } from './brand-logo';

const NAV = [
{ href: '/', label: 'Dashboard', icon: LayoutDashboard },
Expand Down Expand Up @@ -63,11 +64,18 @@ function NavList({ pathname, onNavigate }: { pathname: string; onNavigate?: () =
);
}

function BrandMark({ className }: { className?: string }) {
function AdminBrand({ compact = false }: { compact?: boolean }) {
return (
<span className={cn('font-display text-lg font-semibold tracking-tight text-ink', className)}>
F.A.T.U
</span>
<div className="flex items-center gap-2">
<BrandLogo
className={compact ? 'size-9' : 'size-10'}
priority
sizes={compact ? '36px' : '40px'}
/>
<span className="font-display text-[0.65rem] font-semibold uppercase tracking-[0.16em] text-muted">
Admin
</span>
</div>
);
}

Expand Down Expand Up @@ -135,10 +143,7 @@ export function AdminShell({ children }: { children: React.ReactNode }) {
{/* Desktop sidebar — fixed in the viewport; only main content scrolls */}
<aside className="hidden h-dvh w-60 shrink-0 overflow-y-auto border-r border-line bg-canvas md:block">
<div className="flex h-16 items-center gap-2 border-b border-line px-5">
<BrandMark />
<span className="font-display text-[0.65rem] font-semibold uppercase tracking-[0.16em] text-muted">
Admin
</span>
<AdminBrand />
</div>
<nav className="p-3" aria-label="Admin sections">
<NavList pathname={pathname} />
Expand All @@ -156,10 +161,7 @@ export function AdminShell({ children }: { children: React.ReactNode }) {
aria-haspopup="dialog"
/>
<div className="flex items-center gap-2 md:hidden">
<BrandMark className="text-base" />
<span className="font-display text-[0.65rem] font-semibold uppercase tracking-[0.16em] text-muted">
Admin
</span>
<AdminBrand compact />
</div>
</div>
<div className="flex items-center gap-3">
Expand Down Expand Up @@ -197,12 +199,7 @@ export function AdminShell({ children }: { children: React.ReactNode }) {
className="m-0 h-dvh max-h-none w-[min(20rem,85vw)] max-w-none bg-canvas p-0 text-ink backdrop:bg-black/50 open:flex open:flex-col"
>
<div className="flex h-16 items-center justify-between border-b border-line px-5">
<div className="flex items-center gap-2">
<BrandMark className="text-base" />
<span className="font-display text-[0.65rem] font-semibold uppercase tracking-[0.16em] text-muted">
Admin
</span>
</div>
<AdminBrand compact />
<IconButton
label="Close menu"
icon={<X className="size-5" aria-hidden />}
Expand Down
31 changes: 31 additions & 0 deletions apps/admin/components/brand-logo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { cn } from '@tms/ui';
import Image from 'next/image';

export interface BrandLogoProps {
alt?: string;
className?: string;
priority?: boolean;
sizes?: string;
}

/** The canonical F.A.T.U brand mark, cropped and optimized from the supplied artwork. */
export function BrandLogo({
alt = '',
className,
priority = false,
sizes = '40px',
}: BrandLogoProps) {
return (
<span className={cn('relative inline-flex shrink-0 overflow-hidden', className)}>
<Image
src="/brand/fatu-logo.png"
alt={alt}
width={1024}
height={1024}
priority={priority}
sizes={sizes}
className="size-full object-contain"
/>
</span>
);
}
Binary file added apps/admin/public/brand/fatu-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/storefront/app/apple-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/storefront/app/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 20 additions & 0 deletions apps/storefront/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const sans = IBM_Plex_Sans({

export const metadata: Metadata = {
metadataBase: new URL('https://fromafricatoyou.com'),
applicationName: 'F.A.T.U',
title: {
default: 'From Africa To You — Art-led apparel',
template: '%s · From Africa To You',
Expand All @@ -36,7 +37,26 @@ export const metadata: Metadata = {
siteName: 'From Africa To You',
title: 'From Africa To You — Art-led apparel',
description: 'A premium digital art gallery and interactive clothing-design experience.',
images: [
{
url: '/opengraph-image.png',
width: 1200,
height: 630,
alt: 'F.A.T.U — From Africa To You',
},
],
},
twitter: {
card: 'summary_large_image',
title: 'From Africa To You — Art-led apparel',
description: 'A premium digital art gallery and interactive clothing-design experience.',
images: ['/opengraph-image.png'],
},
icons: {
icon: [{ url: '/icon.png', type: 'image/png', sizes: '512x512' }],
apple: [{ url: '/apple-icon.png', type: 'image/png', sizes: '180x180' }],
},
manifest: '/manifest.webmanifest',
robots: { index: true, follow: true },
};

Expand Down
20 changes: 20 additions & 0 deletions apps/storefront/app/manifest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { MetadataRoute } from 'next';

export default function manifest(): MetadataRoute.Manifest {
return {
name: 'From Africa To You',
short_name: 'F.A.T.U',
description: 'Original African artwork, made wearable.',
start_url: '/',
display: 'standalone',
background_color: '#fafaf7',
theme_color: '#fafaf7',
icons: [
{
src: '/icon.png',
sizes: '512x512',
type: 'image/png',
},
],
};
}
Binary file added apps/storefront/app/opengraph-image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
39 changes: 39 additions & 0 deletions apps/storefront/components/site/brand-logo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { cn } from '@tms/ui';
import Image from 'next/image';

export interface BrandLogoProps {
alt?: string;
className?: string;
inverse?: boolean;
priority?: boolean;
sizes?: string;
}

/** The canonical F.A.T.U brand mark, cropped and optimized from the supplied artwork. */
export function BrandLogo({
alt = '',
className,
inverse = false,
priority = false,
sizes = '48px',
}: BrandLogoProps) {
return (
<span
className={cn(
'relative inline-flex shrink-0 overflow-hidden',
inverse && 'rounded-md bg-[#fafaf7] p-2',
className,
)}
>
<Image
src="/brand/fatu-logo.png"
alt={alt}
width={1024}
height={1024}
priority={priority}
sizes={sizes}
className="size-full object-contain"
/>
</span>
);
}
10 changes: 7 additions & 3 deletions apps/storefront/components/site/site-footer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Container } from '@tms/ui';
import { ArrowRight, Instagram, Twitter } from 'lucide-react';
import Link from 'next/link';
import { BrandLogo } from '@/components/site/brand-logo';
import { footerNav } from '@/lib/nav';

/**
Expand All @@ -25,9 +26,12 @@ export function SiteFooter() {
{/* Mobile: brand full-width, then nav in a 2×2 grid. Desktop: brand + four columns. */}
<div className="grid grid-cols-2 gap-x-6 gap-y-10 lg:grid-cols-[1.4fr_repeat(4,1fr)] lg:gap-12">
<div className="col-span-2 max-w-sm lg:col-span-1">
<span className="font-display text-3xl font-bold uppercase tracking-tight">
F.A.T.U
</span>
<BrandLogo
alt="F.A.T.U — From Africa To You"
className="size-28"
inverse
sizes="112px"
/>
<p className="mt-4 text-sm leading-relaxed text-white/60">
Hand-drawn art from across Africa, printed on cotton. From Africa, to you.
</p>
Expand Down
11 changes: 8 additions & 3 deletions apps/storefront/components/site/site-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { usePathname } from 'next/navigation';
import { type MouseEvent, useCallback, useEffect, useRef } from 'react';
import { useAuth } from '@/components/account/auth-provider';
import { useCart } from '@/components/cart/cart-provider';
import { BrandLogo } from '@/components/site/brand-logo';
import { primaryNav } from '@/lib/nav';

/**
Expand Down Expand Up @@ -62,9 +63,10 @@ export function SiteHeader() {
<nav className="flex h-16 items-center justify-between gap-4" aria-label="Primary">
<Link
href="/"
className="rounded-sm font-display text-lg font-semibold tracking-tight text-ink outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-focus-ring)]"
aria-label="F.A.T.U home"
className="inline-flex size-12 shrink-0 items-center justify-center rounded-sm outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-focus-ring)]"
>
F.A.T.U
<BrandLogo className="size-11" priority sizes="44px" />
</Link>

<ul className="hidden items-center gap-8 md:flex">
Expand Down Expand Up @@ -136,7 +138,10 @@ export function SiteHeader() {
className="tms-slideover m-0 ml-auto h-dvh max-h-none w-[min(22rem,85vw)] max-w-none bg-canvas p-0 text-ink open:flex open:flex-col"
>
<div className="flex h-16 items-center justify-between border-b border-line px-5">
<span className="font-display text-sm font-semibold tracking-tight">Menu</span>
<div className="flex items-center gap-2.5">
<BrandLogo className="size-9" sizes="36px" />
<span className="font-display text-sm font-semibold tracking-tight">Menu</span>
</div>
<IconButton
label="Close menu"
icon={<X className="size-5" aria-hidden />}
Expand Down
Binary file added apps/storefront/public/brand/fatu-logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
27 changes: 25 additions & 2 deletions apps/storefront/tests/visual/showcase.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,36 @@ import { expect, test } from '@playwright/test';
test.describe('storefront foundation', () => {
test('homepage renders hero and gallery', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('heading', { level: 1 })).toContainText('artwork is the hero');
await expect(page.getByRole('heading', { name: 'From the gallery' })).toBeVisible();
await expect(page.getByRole('heading', { level: 1 })).toHaveText(/From Africa,\s*to you\./i);
await expect(page.getByRole('heading', { name: 'Shop by collection' })).toBeVisible();
// Skip link is the first focusable element.
await page.keyboard.press('Tab');
await expect(page.getByRole('link', { name: 'Skip to content' })).toBeFocused();
});

test('brand logo and install metadata are available', async ({ page, request }) => {
await page.goto('/');

const homeLink = page.getByRole('link', { name: 'F.A.T.U home' });
await expect(homeLink).toBeVisible();
await expect(homeLink.locator('img')).toHaveAttribute('src', /fatu-logo\.png/);
await expect(page.getByRole('img', { name: 'F.A.T.U — From Africa To You' })).toBeVisible();

const manifestResponse = await request.get('/manifest.webmanifest');
expect(manifestResponse.ok()).toBeTruthy();
await expect(manifestResponse.json()).resolves.toMatchObject({
name: 'From Africa To You',
short_name: 'F.A.T.U',
icons: [{ src: '/icon.png', sizes: '512x512', type: 'image/png' }],
});

for (const path of ['/icon.png', '/apple-icon.png', '/opengraph-image.png']) {
const response = await request.get(path);
expect(response.ok()).toBeTruthy();
expect(response.headers()['content-type']).toBe('image/png');
}
});

test('visual baseline — homepage', async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('networkidle');
Expand Down
Loading