Skip to content
Open
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
111 changes: 18 additions & 93 deletions app/globals.css
Original file line number Diff line number Diff line change
@@ -1,99 +1,24 @@
@import "tailwindcss";

@layer base {
:root {
--font-clash: "Clash Display", sans-serif;
--font-general-sans: "General Sans", sans-serif;
--font-inter: "Inter", sans-serif;

/* ── Elevation / shadow scale (light) ────────────────────────────────
* Four-step hierarchy that every card-like surface must use.
*
* 1 — Static / default cards
* 2 — Hoverable / interactive cards
* 3 — Floating panels (popovers, dropdowns)
* 4 — Modals / dialogs
*
* Dark-mode values are swapped via the .dark variant below.
* Usage: shadow-elevation-1 … shadow-elevation-4
*/
--elevation-1: 0 1px 2px 0 rgb(0 0 0 / 0.03), 0 1px 1px 0 rgb(0 0 0 / 0.02);
--elevation-2: 0 4px 6px -1px rgb(0 0 0 / 0.05), 0 2px 4px -2px rgb(0 0 0 / 0.03);
--elevation-3: 0 10px 15px -3px rgb(0 0 0 / 0.08), 0 4px 6px -4px rgb(0 0 0 / 0.04);
--elevation-4: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.05);

/* Disabled state token — single source of truth for all disabled controls
* Applied via the cursor-disabled and opactiy-disabled utilities below.
* Used across Button, Checkbox, TextInput, and Input.
*/
--disabled-opacity: 0.5;

/* ── Skeleton / shimmer loading tokens ───────────────────────────────
* Single source of truth for all skeleton animation timing across the
* entire application. Every skeleton primitive (Skeleton, SkeletonLine,
* SkeletonAvatar, etc.) reads these tokens via the .skeleton-shimmer
* class so that a card skeleton, a table skeleton, and any other
* loading placeholder all pulse / shimmer at exactly the same rate.
*
* --shimmer-duration How long one full animation cycle lasts (2s).
* --shimmer-easing The easing curve for the shimmer sweep (ease-in-out).
*
* These tokens are consumed by the `.skeleton-shimmer::after` animation
* below and respected by the prefers-reduced-motion fallback.
* Change these two values to retime every loading placeholder at once.
*/
--shimmer-duration: 2s;
--shimmer-easing: ease-in-out;

/* Section vertical-rhythm spacing tokens
* Applied as py-section-sm / py-section-md / py-section-lg via
* the @utility helpers below, or directly via Tailwind responsive
* classes: py-16 sm:py-20 lg:py-24. These tokens document the
* canonical three-step scale used across every landing section.
*/
--section-py-sm: 4rem; /* 64 px — mobile (≤ 639 px) */
--section-py-md: 5rem; /* 80 px — tablet (≥ 640 px) */
--section-py-lg: 6rem; /* 96 px — desktop (≥ 1024 px) */

/* Clash Display (Display & Headings) Token Scale */
--font-display-2xl: 4.5rem;
--leading-display-2xl: 1.05;
--tracking-display-2xl: -0.03em;

--font-display-xl: 3.75rem;
--leading-display-xl: 1.1;
--tracking-display-xl: -0.025em;

--font-heading-lg: 2.25rem;
--leading-heading-lg: 1.2;
--tracking-heading-lg: -0.02em;

--font-heading-md: 1.5rem;
--leading-heading-md: 1.25;
--tracking-heading-md: -0.015em;

/* General Sans / Inter (Body Copy & UI) Token Scale */
--font-body-lg: 1.125rem;
--leading-body-lg: 1.6;
--tracking-body-lg: -0.01em;

--font-body-md: 1rem;
--leading-body-md: 1.5;
--tracking-body-md: 0em;

--font-caption-sm: 0.875rem;
--leading-caption-sm: 1.43;
--tracking-caption-sm: 0.01em;
/**
* Design Token Migration Reference
*
* This file defines the shared design tokens used across the Stellopay frontend.
* Components should reference these tokens instead of hardcoded hex colors.
*
* Migration Status: See design/token-migration-matrix.md for the current inventory and rollout order.
*
* Token Categories:
* - Colors: background, foreground, semantic (primary, destructive, success, warning)
* - Typography: font families (sans, clash, general)
* - Spacing: border-radius scale (sm, md, lg, xl)
* - Effects: focus ring and shadows
*
* Usage: Reference via CSS custom properties (--color-*, --font-*, --radius-*)
* or via Tailwind classes (bg-primary, text-foreground, rounded-lg, etc.).
*/

design-system/mini-bar-chart-dark-tokens
/* Chart tokens — used by Recharts components */
--chart-1: #4f6fff;
--chart-2: #10b981;
--chart-3: #f59e0b;
--chart-4: #8b5cf6;
--chart-5: #ef4444;
--chart-tooltip-bg: #ffffff;
--chart-tooltip-text: #09090b;
@custom-variant dark (&:is(.dark *));

/* Chart colors — light mode
* Used by RechartsMiniBarChart and other chart components.
Expand Down
63 changes: 63 additions & 0 deletions app/metadata.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { describe, expect, it, vi } from "vitest";
import { readFileSync } from "fs";
import { resolve } from "path";

const { mockLocalFont, mockEnMessages } = vi.hoisted(() => {
// Footer references `messages` as a global without importing it.
Expand Down Expand Up @@ -210,6 +212,67 @@ describe("Route Metadata Exports", () => {
});
});

// ── Design Token Migration Matrix ────────────────────────────────────────────

it("design-token migration matrix document exists and is non-empty", () => {
const matrixPath = resolve(
__dirname,
"../design/token-migration-matrix.md",
);
const content = readFileSync(matrixPath, "utf-8");
expect(content.length).toBeGreaterThan(100);
expect(content).toContain("# Design Token Migration Matrix");
expect(content).toContain("Migration Status Overview");
});

it("design-token migration matrix covers all major component categories", () => {
const matrixPath = resolve(
__dirname,
"../design/token-migration-matrix.md",
);
const content = readFileSync(matrixPath, "utf-8");

// Verify each priority tier is represented
expect(content).toContain("High");
expect(content).toContain("Medium");
expect(content).toContain("Low");

// Verify status indicators are present
expect(content).toContain("Done");
expect(content).toContain("In Progress");
expect(content).toContain("Not Started");
});

it("design-token migration matrix prioritizes landing and dashboard surfaces", () => {
const matrixPath = resolve(
__dirname,
"../design/token-migration-matrix.md",
);
const content = readFileSync(matrixPath, "utf-8");

expect(content).toContain("Landing Hero");
expect(content).toContain("Dashboard Account Summary");
expect(content).toContain("Settings Preferences");
expect(content).toContain("https://github.com/stellopay/frontend/issues/");
});

it("design-token migration matrix inventories representative app and component surfaces", () => {
const matrixPath = resolve(
__dirname,
"../design/token-migration-matrix.md",
);
const content = readFileSync(matrixPath, "utf-8");

expect(content).toContain("app/account-summary/page.tsx");
expect(content).toContain("components/common/footer.tsx");
expect(content).toContain("components/common/navbar.tsx");
});

it("globals.css contains design token documentation comment", () => {
const cssPath = resolve(__dirname, "globals.css");
const content = readFileSync(cssPath, "utf-8");
expect(content).toContain("Design Token Migration Reference");
expect(content).toContain("token-migration-matrix.md");
describe("Landing Page JSON-LD Structured Data", () => {
it("exports a valid structured data object with @graph", () => {
expect(landingStructuredData).toBeDefined();
Expand Down
10 changes: 10 additions & 0 deletions app/metadata.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"use client";

export default function MetadataTest() {
return (
<div className="p-8">
<h1 className="text-2xl font-bold mb-4">Metadata Test Component</h1>
<p className="text-gray-600">This is a test component for metadata exports.</p>
</div>
);
}
29 changes: 10 additions & 19 deletions app/settings/preferences/components/account-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,18 @@ import { Input } from "@/components/ui/input";
import { DestructiveActionDialog } from "./destructive-action-dialog";
import { OAuthCallbackError } from "@/lib/api/auth";

export interface ProfileState {
firstName: string;
lastName: string;
displayName: string;
email: string;
timezone: string;
currency: string;
interface DeletionFlowState {
reason: string | null;
dataExported: boolean;
isDeleting: boolean;
countdown: number;
}

/**
* Default profile values seeded from demo data. Exported so a parent surface
* (e.g. the settings summary cards) can own the same initial state when it
* lifts this section into a controlled component.
*/
export const DEFAULT_PROFILE: ProfileState = {
firstName: DEMO_PROFILE.firstName,
lastName: DEMO_PROFILE.lastName,
displayName: DEMO_PROFILE.displayName,
email: DEMO_PROFILE.email,
timezone: DEMO_PROFILE.timezone,
currency: DEMO_PROFILE.currency,
const DEFAULT_DELETION_FLOW: DeletionFlowState = {
reason: null,
dataExported: false,
isDeleting: false,
countdown: 14
};

/** Number of profile fields that have a non-empty value. */
Expand Down
12 changes: 9 additions & 3 deletions components/auth/auth-social-buttons.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,12 @@ describe("AuthSocialButtons", () => {
expect(appleBtn).not.toBeDisabled();
});

// ── OAuth callback error states ────────────────────────────────────────────

describe("OAuth callback error states", () => {
const mockSimulateOAuth = vi.mocked(
await import("@/lib/api/auth")
).simulateOAuth;
// ── Divider accessibility ──────────────────────────────────────────────────

it("renders a sr-only separator with role='separator' and an accessible label", () => {
Expand Down Expand Up @@ -345,7 +351,7 @@ describe("AuthSocialButtons", () => {

await userEvent.click(googleBtn);

expect(screen.getAllByText(/denied permission/i).length).toBeGreaterThanOrEqual(1);
expect(screen.getByText(/denied permission/i)).toBeInTheDocument();
expect(screen.getByText(/user has denied permission/i)).toBeInTheDocument();
expect(screen.getByRole("button", { name: /retry/i })).toBeInTheDocument();
expect(screen.getByRole("button", { name: /use email instead/i })).toBeInTheDocument();
Expand All @@ -366,8 +372,8 @@ describe("AuthSocialButtons", () => {

await userEvent.click(googleBtn);

expect(screen.getAllByText(/temporarily unavailable/i).length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText(/authentication provider is temporarily unavailable/i).length).toBeGreaterThanOrEqual(1);
expect(screen.getByText(/temporarily unavailable/i)).toBeInTheDocument();
expect(screen.getByText(/authentication provider is temporarily unavailable/i)).toBeInTheDocument();
expect(screen.getByRole("button", { name: /retry/i })).toBeInTheDocument();
expect(screen.getByRole("button", { name: /use email instead/i })).toBeInTheDocument();
});
Expand Down
Loading
Loading