design: apply Pixel design system tokens (Fira Sans/Code, CSS vars, badge helpers); add Playwright E2E setup + auth spec
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -34,3 +34,8 @@ yarn-error.log*
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# Playwright
|
||||
playwright-report/
|
||||
test-results/
|
||||
.auth/
|
||||
|
||||
128
app/globals.css
128
app/globals.css
@@ -2,63 +2,101 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* FiberOps Design System — Pixel/Conan approved */
|
||||
/* Primary: #0891B2 | CTA: #059669 | BG: #F8FAFC | Text: #0F172A */
|
||||
/* Fonts: Fira Sans (body) + Fira Code (data/numbers) */
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 222.2 84% 4.9%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
/* FiberOps tokens */
|
||||
--color-primary: #0891B2;
|
||||
--color-primary-dark: #0E7490;
|
||||
--color-secondary: #22D3EE;
|
||||
--color-cta: #059669;
|
||||
--color-cta-hover: #047857;
|
||||
--color-bg: #F8FAFC;
|
||||
--color-surface: #FFFFFF;
|
||||
--color-text: #0F172A;
|
||||
--color-muted: #64748B;
|
||||
--color-border: #E2E8F0;
|
||||
--color-danger: #EF4444;
|
||||
--color-warning: #F59E0B;
|
||||
--color-success: #059669;
|
||||
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
/* Shadows */
|
||||
--shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
|
||||
--shadow-md: 0 4px 6px rgba(0,0,0,0.07);
|
||||
--shadow-lg: 0 10px 15px rgba(0,0,0,0.08);
|
||||
--shadow-xl: 0 20px 25px rgba(0,0,0,0.12);
|
||||
|
||||
/* Radius */
|
||||
--radius-sm: 6px;
|
||||
--radius-md: 10px;
|
||||
--radius-lg: 14px;
|
||||
--radius-xl: 20px;
|
||||
}
|
||||
|
||||
* {
|
||||
@apply border-border;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
font-family: 'Fira Sans', 'Inter', system-ui, sans-serif;
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* Data values, numbers, IDs */
|
||||
code, .font-mono, .tabular-nums {
|
||||
font-family: 'Fira Code', 'JetBrains Mono', monospace;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.text-balance {
|
||||
text-wrap: balance;
|
||||
/* Skeleton loading animation */
|
||||
.skeleton {
|
||||
@apply animate-pulse bg-slate-200 rounded;
|
||||
}
|
||||
|
||||
/* Row hover for tables */
|
||||
.table-row-hover:hover {
|
||||
background-color: #F0FDFF;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Transition helper */
|
||||
.transition-fast {
|
||||
transition: all 150ms ease;
|
||||
}
|
||||
|
||||
.transition-smooth {
|
||||
transition: all 200ms ease;
|
||||
}
|
||||
|
||||
/* Card base */
|
||||
.fiberops-card {
|
||||
background: white;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--color-border);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
/* Status badge helpers */
|
||||
.badge-active { background: #DCFCE7; color: #166534; }
|
||||
.badge-inactive { background: #F1F5F9; color: #475569; }
|
||||
.badge-suspended { background: #FEF3C7; color: #92400E; }
|
||||
.badge-pending { background: #DBEAFE; color: #1E40AF; }
|
||||
.badge-overdue { background: #FEE2E2; color: #991B1B; }
|
||||
.badge-paid { background: #DCFCE7; color: #166534; }
|
||||
.badge-partial { background: #FEF3C7; color: #92400E; }
|
||||
.badge-void { background: #F1F5F9; color: #94A3B8; }
|
||||
.badge-open { background: #DBEAFE; color: #1E40AF; }
|
||||
.badge-in-progress { background: #FEF3C7; color: #92400E; }
|
||||
.badge-resolved { background: #DCFCE7; color: #166534; }
|
||||
.badge-closed { background: #F1F5F9; color: #475569; }
|
||||
|
||||
.text-balance { text-wrap: balance; }
|
||||
}
|
||||
|
||||
@@ -1,23 +1,29 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { Inter } from 'next/font/google';
|
||||
import { Fira_Sans, Fira_Code } from 'next/font/google';
|
||||
import './globals.css';
|
||||
import Providers from '@/components/providers';
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] });
|
||||
const firaSans = Fira_Sans({
|
||||
subsets: ['latin'],
|
||||
weight: ['300', '400', '500', '600', '700'],
|
||||
variable: '--font-fira-sans',
|
||||
});
|
||||
|
||||
const firaCode = Fira_Code({
|
||||
subsets: ['latin'],
|
||||
weight: ['400', '500', '600', '700'],
|
||||
variable: '--font-fira-code',
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'FiberOps Admin',
|
||||
description: 'ISP Management Platform',
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>
|
||||
<body className={`${firaSans.variable} ${firaCode.variable} ${firaSans.className}`}>
|
||||
<Providers>{children}</Providers>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
39
e2e/auth.spec.ts
Normal file
39
e2e/auth.spec.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { login, DEMO } from './helpers/auth';
|
||||
|
||||
test.describe('Authentication', () => {
|
||||
test('login page renders correctly', async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
await expect(page.locator('text=FiberOps')).toBeVisible();
|
||||
await expect(page.locator('text=Sign in to your account')).toBeVisible();
|
||||
await expect(page.locator('input[placeholder="e.g. demo-isp"]')).toBeVisible();
|
||||
await expect(page.locator('input[type="email"]')).toBeVisible();
|
||||
await expect(page.locator('input[type="password"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('login with valid credentials redirects to dashboard', async ({ page }) => {
|
||||
await login(page);
|
||||
await expect(page).toHaveURL(/\/dashboard/);
|
||||
});
|
||||
|
||||
test('login with wrong credentials shows error', async ({ page }) => {
|
||||
await page.goto('/login');
|
||||
await page.fill('input[placeholder="e.g. demo-isp"]', DEMO.tenant);
|
||||
await page.fill('input[type="email"]', DEMO.email);
|
||||
await page.fill('input[type="password"]', 'wrongpassword');
|
||||
await page.click('button[type="submit"]');
|
||||
// Should show error toast or stay on login
|
||||
await expect(page).toHaveURL(/\/login/);
|
||||
});
|
||||
|
||||
test('unauthenticated redirect to login', async ({ page }) => {
|
||||
await page.goto('/dashboard');
|
||||
await expect(page).toHaveURL(/\/login/);
|
||||
});
|
||||
|
||||
test('logout returns to login', async ({ page }) => {
|
||||
await login(page);
|
||||
await page.click('button:has-text("Logout"), a:has-text("Logout")');
|
||||
await expect(page).toHaveURL(/\/login/);
|
||||
});
|
||||
});
|
||||
16
e2e/helpers/auth.ts
Normal file
16
e2e/helpers/auth.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Page } from '@playwright/test';
|
||||
|
||||
export const DEMO = {
|
||||
tenant: 'demo-isp',
|
||||
email: 'admin@demo-isp.com',
|
||||
password: 'Admin123!',
|
||||
};
|
||||
|
||||
export async function login(page: Page, creds = DEMO) {
|
||||
await page.goto('/login');
|
||||
await page.fill('input[placeholder="e.g. demo-isp"]', creds.tenant);
|
||||
await page.fill('input[type="email"]', creds.email);
|
||||
await page.fill('input[type="password"]', creds.password);
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForURL('**/dashboard', { timeout: 10000 });
|
||||
}
|
||||
64
package-lock.json
generated
64
package-lock.json
generated
@@ -38,6 +38,7 @@
|
||||
"zustand": "^5.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
@@ -1547,6 +1548,22 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.58.2",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
|
||||
"integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.58.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/number": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
|
||||
@@ -8376,6 +8393,53 @@
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.58.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
|
||||
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.58.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.58.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
|
||||
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright/node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/possible-typed-array-names": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"zustand": "^5.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
|
||||
21
playwright.config.ts
Normal file
21
playwright.config.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
fullyParallel: false, // run serially — shared demo tenant
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: 1,
|
||||
workers: 1,
|
||||
reporter: [['html', { outputFolder: 'playwright-report' }], ['line']],
|
||||
use: {
|
||||
baseURL: process.env.E2E_BASE_URL || 'http://192.168.1.167:3002',
|
||||
trace: 'on-first-retry',
|
||||
screenshot: 'only-on-failure',
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'], headless: true },
|
||||
},
|
||||
],
|
||||
});
|
||||
Reference in New Issue
Block a user