-
- RDG
-
+
RentalDriveGo
{footer.tagline}
diff --git a/homepage/eslint.config.mjs b/homepage/eslint.config.mjs
deleted file mode 100644
index da90fbc..0000000
--- a/homepage/eslint.config.mjs
+++ /dev/null
@@ -1,40 +0,0 @@
-import { defineConfig, globalIgnores } from 'eslint/config';
-import nextVitals from 'eslint-config-next/core-web-vitals';
-import nextTypeScript from 'eslint-config-next/typescript';
-import prettier from 'eslint-config-prettier';
-
-export default defineConfig([
- ...nextVitals,
- ...nextTypeScript,
- prettier,
- {
- rules: {
- 'no-console': ['error', { allow: ['log', 'warn', 'error'] }],
- },
- },
- {
- files: ['**/*.{ts,tsx}'],
- languageOptions: {
- parserOptions: {
- projectService: true,
- tsconfigRootDir: import.meta.dirname,
- },
- },
- rules: {
- '@typescript-eslint/consistent-type-imports': [
- 'error',
- { prefer: 'type-imports', fixStyle: 'inline-type-imports' },
- ],
- '@typescript-eslint/no-floating-promises': 'error',
- '@typescript-eslint/switch-exhaustiveness-check': 'error',
- },
- },
- globalIgnores([
- '.next/**',
- 'coverage/**',
- 'playwright-report/**',
- 'test-results/**',
- 'contracts/**',
- 'next-env.d.ts',
- ]),
-]);
diff --git a/homepage/next-env.d.ts b/homepage/next-env.d.ts
deleted file mode 100644
index c4b7818..0000000
--- a/homepage/next-env.d.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-///
-///
-import "./.next/dev/types/routes.d.ts";
-
-// NOTE: This file should not be edited
-// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/homepage/next.config.ts b/homepage/next.config.ts
deleted file mode 100644
index df98054..0000000
--- a/homepage/next.config.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-import type { NextConfig } from 'next';
-
-const securityHeaders = [
- { key: 'Strict-Transport-Security', value: 'max-age=31536000; includeSubDomains' },
- { key: 'X-Content-Type-Options', value: 'nosniff' },
- { key: 'X-Frame-Options', value: 'DENY' },
- { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
- { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
-];
-
-const nextConfig: NextConfig = {
- allowedDevOrigins: ['192.168.3.3'],
- compress: true,
- poweredByHeader: false,
- reactStrictMode: true,
-
- transpilePackages: ['@rentaldrivego/types'],
-
- images: {
- remotePatterns: [
- {
- protocol: 'https',
- hostname: 'res.cloudinary.com',
- },
- ],
- },
-
- async headers() {
- return [
- {
- source: '/:path*',
- headers: securityHeaders,
- },
- ];
- },
-
- async redirects() {
- return [
- {
- source: '/dashboard/dashboard',
- destination: '/dashboard',
- permanent: false,
- },
- {
- source: '/dashboard/dashboard/:path*',
- destination: '/dashboard/:path*',
- permanent: false,
- },
- ];
- },
-
- async rewrites() {
- const dashboardOrigin =
- process.env.DASHBOARD_INTERNAL_URL ?? 'http://dashboard:3001';
- const adminOrigin =
- process.env.ADMIN_INTERNAL_URL ?? 'http://admin:3002';
-
- return [
- {
- source: '/dashboard',
- destination: `${dashboardOrigin}/dashboard`,
- },
- {
- source: '/dashboard/:path*',
- destination: `${dashboardOrigin}/dashboard/:path*`,
- },
- {
- source: '/admin',
- destination: `${adminOrigin}/admin`,
- },
- {
- source: '/admin/:path*',
- destination: `${adminOrigin}/admin/:path*`,
- },
- ];
- },
-};
-
-export default nextConfig;
diff --git a/homepage/package.json b/homepage/package.json
deleted file mode 100644
index e8e059b..0000000
--- a/homepage/package.json
+++ /dev/null
@@ -1,49 +0,0 @@
-{
- "name": "@rentaldrivego/homepage",
- "version": "1.0.0",
- "private": true,
- "scripts": {
- "predev": "npm run build --workspace @rentaldrivego/types",
- "dev": "next dev -H 0.0.0.0 -p 3000",
- "prebuild": "npm run build --workspace @rentaldrivego/types",
- "build": "next build",
- "prestart": "npm run build --workspace @rentaldrivego/types",
- "pretype-check": "npm run build --workspace @rentaldrivego/types",
- "start": "next start -H 0.0.0.0 -p 3000",
- "type-check": "tsc --noEmit",
- "test": "vitest run",
- "test:watch": "vitest",
- "lint": "eslint . --max-warnings=0",
- "format": "prettier --write .",
- "format:check": "prettier --check ."
- },
- "dependencies": {
- "@fontsource-variable/inter": "^5.2.8",
- "@fontsource-variable/noto-sans-arabic": "^5.2.10",
- "@rentaldrivego/types": "*",
- "next": "^16.2.7",
- "react": "^19.2.0",
- "react-dom": "^19.2.0",
- "zod": "^4.4.0"
- },
- "devDependencies": {
- "@eslint/js": "^9.0.0",
- "@playwright/test": "^1.61.0",
- "@testing-library/jest-dom": "^6.9.0",
- "@testing-library/react": "^16.3.0",
- "@testing-library/user-event": "^14.6.0",
- "@types/node": "^20.12.0",
- "@types/react": "^19.2.0",
- "@types/react-dom": "^19.2.0",
- "@vitest/coverage-v8": "^4.1.0",
- "eslint": "^9.0.0",
- "eslint-config-next": "^16.2.0",
- "eslint-config-prettier": "^10.1.0",
- "globals": "^17.0.0",
- "jsdom": "^29.0.0",
- "prettier": "^3.8.0",
- "typescript": "^5.4.0",
- "typescript-eslint": "^8.0.0",
- "vitest": "^4.1.0"
- }
-}
diff --git a/homepage/playwright.config.ts b/homepage/playwright.config.ts
deleted file mode 100644
index 39ab4a6..0000000
--- a/homepage/playwright.config.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import { defineConfig, devices } from '@playwright/test';
-
-const baseURL = process.env.PLAYWRIGHT_BASE_URL ?? 'http://127.0.0.1:3000';
-const executablePath = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH;
-const serverCommand = process.env.PLAYWRIGHT_SERVER_COMMAND ?? 'pnpm start';
-
-export default defineConfig({
- testDir: './tests',
- testMatch: ['browser/**/*.spec.ts', 'a11y/**/*.spec.ts', 'visual/**/*.spec.ts'],
- fullyParallel: true,
- forbidOnly: Boolean(process.env.CI),
- retries: process.env.CI ? 2 : 0,
- ...(process.env.CI ? { workers: 2 } : {}),
- reporter: process.env.CI ? [['html', { open: 'never' }], ['github']] : 'list',
- expect: {
- timeout: 5_000,
- toHaveScreenshot: {
- animations: 'disabled',
- caret: 'hide',
- maxDiffPixelRatio: 0.005,
- },
- },
- use: {
- baseURL,
- locale: 'en-US',
- timezoneId: 'UTC',
- trace: 'retain-on-failure',
- screenshot: 'only-on-failure',
- video:
- process.env.PLAYWRIGHT_VIDEO === 'off' ? 'off' : process.env.CI ? 'retain-on-failure' : 'off',
- },
- projects: [
- {
- name: 'chromium',
- use: {
- ...devices['Desktop Chrome'],
- ...(executablePath ? { launchOptions: { executablePath } } : {}),
- },
- },
- { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
- { name: 'webkit', use: { ...devices['Desktop Safari'] } },
- { name: 'mobile-chrome', use: { ...devices['Pixel 7'] } },
- { name: 'mobile-safari', use: { ...devices['iPhone 15'] } },
- ],
- webServer: {
- command: serverCommand,
- url: baseURL,
- reuseExistingServer: !process.env.CI,
- timeout: 120_000,
- env: {
- SITE_ORIGIN: baseURL,
- PUBLIC_RELEASE_APPROVED: 'false',
- COMPONENT_FIXTURES_ENABLED: 'true',
- NEXT_PUBLIC_DEMO_ENABLED: 'true',
- DEMO_SUBMISSION_MODE: 'local',
- DEMO_LOCAL_TEST_MODE: 'true',
- DEMO_TEST_SCENARIOS_ENABLED: 'true',
- NEXT_PUBLIC_ANALYTICS_MODE: 'disabled',
- HOSTNAME: '127.0.0.1',
- PORT: '3000',
- },
- },
-});
diff --git a/homepage/prettier.config.mjs b/homepage/prettier.config.mjs
deleted file mode 100644
index b1f4351..0000000
--- a/homepage/prettier.config.mjs
+++ /dev/null
@@ -1,9 +0,0 @@
-/** @type {import("prettier").Config} */
-const config = {
- printWidth: 100,
- singleQuote: true,
- trailingComma: 'all',
- plugins: [],
-};
-
-export default config;
diff --git a/homepage/public/.gitkeep b/homepage/public/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/homepage/public/benefit1_AR.png b/homepage/public/benefit1_AR.png
deleted file mode 100644
index bc7f5b0..0000000
Binary files a/homepage/public/benefit1_AR.png and /dev/null differ
diff --git a/homepage/public/benefit1_EN.png b/homepage/public/benefit1_EN.png
deleted file mode 100644
index 5cd3d5d..0000000
Binary files a/homepage/public/benefit1_EN.png and /dev/null differ
diff --git a/homepage/public/benefit1_FR.png b/homepage/public/benefit1_FR.png
deleted file mode 100644
index 0288fa7..0000000
Binary files a/homepage/public/benefit1_FR.png and /dev/null differ
diff --git a/homepage/public/dashboard1_AR.png b/homepage/public/dashboard1_AR.png
deleted file mode 100644
index 5b1fd4c..0000000
Binary files a/homepage/public/dashboard1_AR.png and /dev/null differ
diff --git a/homepage/public/dashboard1_EN.png b/homepage/public/dashboard1_EN.png
deleted file mode 100644
index d48c2c6..0000000
Binary files a/homepage/public/dashboard1_EN.png and /dev/null differ
diff --git a/homepage/public/dashboard1_FR.png b/homepage/public/dashboard1_FR.png
deleted file mode 100644
index c6f0e28..0000000
Binary files a/homepage/public/dashboard1_FR.png and /dev/null differ
diff --git a/homepage/public/dashboard2_AR.png b/homepage/public/dashboard2_AR.png
deleted file mode 100644
index 7ce0783..0000000
Binary files a/homepage/public/dashboard2_AR.png and /dev/null differ
diff --git a/homepage/public/dashboard2_EN.png b/homepage/public/dashboard2_EN.png
deleted file mode 100644
index 2ece4ea..0000000
Binary files a/homepage/public/dashboard2_EN.png and /dev/null differ
diff --git a/homepage/public/dashboard2_FR.png b/homepage/public/dashboard2_FR.png
deleted file mode 100644
index 1dac45f..0000000
Binary files a/homepage/public/dashboard2_FR.png and /dev/null differ
diff --git a/homepage/public/dashboard3_EN.png b/homepage/public/dashboard3_EN.png
deleted file mode 100644
index f24b917..0000000
Binary files a/homepage/public/dashboard3_EN.png and /dev/null differ
diff --git a/homepage/public/dashboard4_EN.png b/homepage/public/dashboard4_EN.png
deleted file mode 100644
index e78c73c..0000000
Binary files a/homepage/public/dashboard4_EN.png and /dev/null differ
diff --git a/homepage/public/dashboard5_AR.png b/homepage/public/dashboard5_AR.png
deleted file mode 100644
index 3fb688e..0000000
Binary files a/homepage/public/dashboard5_AR.png and /dev/null differ
diff --git a/homepage/public/dashboard5_EN.png b/homepage/public/dashboard5_EN.png
deleted file mode 100644
index 76bc626..0000000
Binary files a/homepage/public/dashboard5_EN.png and /dev/null differ
diff --git a/homepage/public/dashboard5_FR.png b/homepage/public/dashboard5_FR.png
deleted file mode 100644
index 178eaad..0000000
Binary files a/homepage/public/dashboard5_FR.png and /dev/null differ
diff --git a/homepage/public/oilchange1_AR.png b/homepage/public/oilchange1_AR.png
deleted file mode 100644
index c8020b5..0000000
Binary files a/homepage/public/oilchange1_AR.png and /dev/null differ
diff --git a/homepage/public/oilchange1_EN.png b/homepage/public/oilchange1_EN.png
deleted file mode 100644
index 92d4bd3..0000000
Binary files a/homepage/public/oilchange1_EN.png and /dev/null differ
diff --git a/homepage/public/oilchange1_FR.png b/homepage/public/oilchange1_FR.png
deleted file mode 100644
index 47d3d72..0000000
Binary files a/homepage/public/oilchange1_FR.png and /dev/null differ
diff --git a/homepage/public/rentaldrivego.png b/homepage/public/rentaldrivego.png
deleted file mode 100644
index 24f50ea..0000000
Binary files a/homepage/public/rentaldrivego.png and /dev/null differ
diff --git a/homepage/public/workflow1_AR.png b/homepage/public/workflow1_AR.png
deleted file mode 100644
index 28a9b66..0000000
Binary files a/homepage/public/workflow1_AR.png and /dev/null differ
diff --git a/homepage/public/workflow1_EN.png b/homepage/public/workflow1_EN.png
deleted file mode 100644
index dc0ee4a..0000000
Binary files a/homepage/public/workflow1_EN.png and /dev/null differ
diff --git a/homepage/public/workflow1_FR.png b/homepage/public/workflow1_FR.png
deleted file mode 100644
index 328b9ff..0000000
Binary files a/homepage/public/workflow1_FR.png and /dev/null differ
diff --git a/homepage/src/app/[locale]/[slug]/page.tsx b/homepage/src/app/[locale]/[slug]/page.tsx
deleted file mode 100644
index ad928cd..0000000
--- a/homepage/src/app/[locale]/[slug]/page.tsx
+++ /dev/null
@@ -1,89 +0,0 @@
-import { StateAction, StateActions, StateShell } from '@/components/app-shell/StateShell';
-import { SignInForm } from '@/components/auth/SignInForm';
-import { ForgotPasswordForm } from '@/components/auth/ForgotPasswordForm';
-import { ResetPasswordForm } from '@/components/auth/ResetPasswordForm';
-import {
- isLocale,
- localizedPath,
- routeIdFromSlug,
- type Locale,
- type RouteId,
-} from '@/lib/localization/config';
-import { getMessages, type ShellMessages } from '@/lib/localization/messages';
-import { buildLocalizedMetadata } from '@/lib/metadata/build-metadata';
-import type { Metadata } from 'next';
-import { notFound } from 'next/navigation';
-
-interface PendingRouteProps {
- params: Promise<{ locale: string; slug: string }>;
-}
-
-function routeLabel(messages: ShellMessages, routeId: RouteId): string {
- const labels = messages.footer.links;
- switch (routeId) {
- case 'sign-in':
- return labels.login;
- case 'forgot-password':
- return 'Forgot password';
- case 'reset-password':
- return 'Reset password';
- case 'privacy':
- return labels.privacy;
- case 'terms':
- return labels.terms;
- case 'accessibility':
- return labels.accessibility;
- case 'home':
- return 'RentalDriveGo';
- }
-}
-
-async function resolveParams(params: PendingRouteProps['params']) {
- const { locale: localeValue, slug } = await params;
- if (!isLocale(localeValue)) notFound();
- const routeId = routeIdFromSlug(localeValue, slug);
- if (!routeId || routeId === 'home') notFound();
- return { locale: localeValue as Locale, routeId };
-}
-
-export async function generateMetadata({ params }: PendingRouteProps): Promise
{
- const { locale, routeId } = await resolveParams(params);
- const messages = getMessages(locale).shell;
- const label = routeLabel(messages, routeId);
- return buildLocalizedMetadata({
- locale,
- routeId,
- title: `${label} | ${messages.metadata.pendingTitle}`,
- description: messages.metadata.pendingDescription,
- indexable: false,
- });
-}
-
-export default async function PendingRoute({ params }: PendingRouteProps) {
- const { locale, routeId } = await resolveParams(params);
- const messages = getMessages(locale).shell;
- const label = routeLabel(messages, routeId);
-
- if (routeId === 'sign-in') {
- return ;
- }
-
- if (routeId === 'forgot-password') {
- return ;
- }
-
- if (routeId === 'reset-password') {
- return ;
- }
-
- return (
-
-
- {messages.states.backHome}
-
-
- );
-}
diff --git a/homepage/src/app/[locale]/component-lab/ComponentLabInteractive.tsx b/homepage/src/app/[locale]/component-lab/ComponentLabInteractive.tsx
deleted file mode 100644
index 1cba5e1..0000000
--- a/homepage/src/app/[locale]/component-lab/ComponentLabInteractive.tsx
+++ /dev/null
@@ -1,96 +0,0 @@
-'use client';
-
-import { Button } from '@/components/actions/Button';
-import { Accordion } from '@/components/controls/Accordion';
-import { SegmentedControl } from '@/components/controls/SegmentedControl';
-import { Tabs } from '@/components/controls/Tabs';
-import { Alert } from '@/components/feedback/Alert';
-import { Checkbox, Switch } from '@/components/forms/ChoiceControls';
-import { FormField } from '@/components/forms/FormField';
-import { TextInput } from '@/components/forms/TextInput';
-import { DropdownMenu } from '@/components/overlays/DropdownMenu';
-import { Dialog } from '@/components/overlays/Dialog';
-import type { ComponentLabCopy } from '@/content/development/component-lab';
-import { useRef, useState } from 'react';
-import styles from './page.module.css';
-
-export function ComponentLabInteractive({ copy }: { copy: ComponentLabCopy }) {
- const [dialogOpen, setDialogOpen] = useState(false);
- const [segment, setSegment] = useState('comfortable');
- const triggerRef = useRef(null);
- return (
-
-
- Actions
-
-
-
-
-
-
- undefined },
- {
- id: 'archive',
- label: copy.menu.second,
- onSelect: () => undefined,
- destructive: true,
- },
- ]}
- />
-
-
-
-
- Selection and disclosure
-
-
-
-
- {copy.alert.body}
-
-
-
- );
-}
diff --git a/homepage/src/app/[locale]/component-lab/page.module.css b/homepage/src/app/[locale]/component-lab/page.module.css
deleted file mode 100644
index 52517f5..0000000
--- a/homepage/src/app/[locale]/component-lab/page.module.css
+++ /dev/null
@@ -1,32 +0,0 @@
-.main {
- min-block-size: 100dvh;
-}
-.interactiveGrid {
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: var(--space-6);
-}
-.group {
- display: grid;
- align-content: start;
- gap: var(--space-4);
- min-inline-size: 0;
- padding: var(--space-6);
- border: 1px solid var(--border-standard);
- border-radius: var(--radius-md);
- background: var(--surface-elevated);
-}
-.group h2 {
- margin: 0;
- font-size: var(--type-heading-3-size);
-}
-.cluster {
- display: flex;
- flex-wrap: wrap;
- gap: var(--space-3);
-}
-@media (max-width: 767px) {
- .interactiveGrid {
- grid-template-columns: 1fr;
- }
-}
diff --git a/homepage/src/app/[locale]/component-lab/page.tsx b/homepage/src/app/[locale]/component-lab/page.tsx
deleted file mode 100644
index 120134c..0000000
--- a/homepage/src/app/[locale]/component-lab/page.tsx
+++ /dev/null
@@ -1,81 +0,0 @@
-import { Container, Section, Stack } from '@/components/layout/LayoutPrimitives';
-import { Comparison } from '@/components/marketing/Comparison';
-import { CTA } from '@/components/marketing/CTA';
-import { Metric } from '@/components/marketing/Evidence';
-import { ProductPreview } from '@/components/marketing/ProductPreview';
-import { SectionHeader } from '@/components/marketing/SectionHeader';
-import { Workflow } from '@/components/marketing/Workflow';
-import { getComponentLabCopy } from '@/content/development/component-lab';
-import { isLocale, type Locale } from '@/lib/localization/config';
-import { notFound } from 'next/navigation';
-import { ComponentLabInteractive } from './ComponentLabInteractive';
-import styles from './page.module.css';
-
-export const metadata = { robots: { index: false, follow: false } };
-
-export default async function ComponentLabPage({
- params,
-}: {
- params: Promise<{ locale: string }>;
-}) {
- if (process.env.COMPONENT_FIXTURES_ENABLED !== 'true') notFound();
- const { locale } = await params;
- if (!isLocale(locale)) notFound();
- const resolvedLocale: Locale = locale;
- const copy = getComponentLabCopy(resolvedLocale);
- return (
-
-
-
-
-
-
-
-
- ({
- ...row,
- statusTone: index === 0 ? 'success' : 'warning',
- }))}
- />
-
-
-
-
-
-
- );
-}
diff --git a/homepage/src/app/[locale]/error.tsx b/homepage/src/app/[locale]/error.tsx
deleted file mode 100644
index d516598..0000000
--- a/homepage/src/app/[locale]/error.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-'use client';
-
-import { StateButton, StateActions, StateShell } from '@/components/app-shell/StateShell';
-import { getClientShellMessages } from '@/lib/localization/client-messages';
-import { useParams } from 'next/navigation';
-
-export default function ErrorPage({
- reset,
-}: {
- error: Error & { digest?: string };
- reset: () => void;
-}) {
- const params = useParams<{ locale?: string }>();
- const messages = getClientShellMessages(params?.locale);
-
- return (
-
-
- {messages.states.retry}
-
-
- );
-}
diff --git a/homepage/src/app/[locale]/layout.tsx b/homepage/src/app/[locale]/layout.tsx
deleted file mode 100644
index d094193..0000000
--- a/homepage/src/app/[locale]/layout.tsx
+++ /dev/null
@@ -1,99 +0,0 @@
-import '@fontsource-variable/inter/index.css';
-import '@fontsource-variable/noto-sans-arabic/index.css';
-import '@/styles/tokens.css';
-import '@/styles/component-tokens.css';
-import '@/styles/globals.css';
-
-import { SiteFooter } from '@/components/app-shell/SiteFooter';
-import { DemoDialogHost } from '@/components/integrations/DemoDialogHost';
-import { SiteHeader } from '@/components/app-shell/SiteHeader';
-import { ThemeController } from '@/components/app-shell/ThemeController';
-import { getPublicIntegrationConfig } from '@/lib/integrations/environment';
-import { getDirection, isLocale, type Locale } from '@/lib/localization/config';
-import { getMessages } from '@/lib/localization/messages';
-import { themeBootstrapScript } from '@/lib/theme/bootstrap-script';
-import {
- isThemePreference,
- serverResolvedTheme,
- themeCookie,
- type ThemePreference,
-} from '@/lib/theme/config';
-import { cookies, headers } from 'next/headers';
-import { notFound } from 'next/navigation';
-import type { Metadata } from 'next';
-import type { ReactNode } from 'react';
-
-export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise {
- const { locale } = await params;
- return {
- icons: {
- icon: '/rentaldrivego.png',
- apple: '/rentaldrivego.png',
- },
- };
-}
-
-export const dynamic = 'force-dynamic';
-
-interface LocaleLayoutProps {
- children: ReactNode;
- params: Promise<{ locale: string }>;
-}
-
-export default async function LocaleLayout({ children, params }: LocaleLayoutProps) {
- const { locale: localeValue } = await params;
- if (!isLocale(localeValue)) notFound();
- const locale: Locale = localeValue;
- const messages = getMessages(locale);
- const integrationConfig = getPublicIntegrationConfig();
- const requestHeaders = await headers();
- const nonce = requestHeaders.get('x-nonce') ?? undefined;
- const cookieStore = await cookies();
- const cookiePreference = cookieStore.get(themeCookie)?.value;
- const preference: ThemePreference = isThemePreference(cookiePreference)
- ? cookiePreference
- : 'system';
- const resolvedTheme = serverResolvedTheme(preference);
-
- return (
-
-
-
-
-
-
-
- {messages.shell.skipToContent}
-
-
- {children}
-
-
-
-
- );
-}
diff --git a/homepage/src/app/[locale]/loading.tsx b/homepage/src/app/[locale]/loading.tsx
deleted file mode 100644
index 12cd6ed..0000000
--- a/homepage/src/app/[locale]/loading.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import { LocalizedLoading } from '@/components/app-shell/LocalizedLoading';
-
-export default function Loading() {
- return ;
-}
diff --git a/homepage/src/app/[locale]/not-found.tsx b/homepage/src/app/[locale]/not-found.tsx
deleted file mode 100644
index 83b5b9a..0000000
--- a/homepage/src/app/[locale]/not-found.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import { LocalizedNotFound } from '@/components/app-shell/LocalizedNotFound';
-
-export default function NotFound() {
- return ;
-}
diff --git a/homepage/src/app/[locale]/page.tsx b/homepage/src/app/[locale]/page.tsx
deleted file mode 100644
index f27f88c..0000000
--- a/homepage/src/app/[locale]/page.tsx
+++ /dev/null
@@ -1,352 +0,0 @@
-import { ActionLink } from '@/components/actions/ActionLink';
-import { Accordion } from '@/components/controls/Accordion';
-import { Icon } from '@/components/foundation/Icon';
-import { DemoTrigger } from '@/components/integrations/DemoTrigger';
-import { Container, Section, Stack } from '@/components/layout/LayoutPrimitives';
-import { CTA } from '@/components/marketing/CTA';
-import { Comparison } from '@/components/marketing/Comparison';
-import { FeatureCard } from '@/components/marketing/FeatureCard';
-import { ProductPreview } from '@/components/marketing/ProductPreview';
-import { SectionHeader } from '@/components/marketing/SectionHeader';
-import { FeatureGrid } from '@/components/marketing/SectionPatterns';
-import { Workflow } from '@/components/marketing/Workflow';
-import { Callout } from '@/components/surfaces/Callout';
-import { Card } from '@/components/surfaces/Card';
-import { Eyebrow, Heading, Text } from '@/components/typography/Typography';
-import { buildHomepageContent } from '@/content/homepage-model';
-import { getPublicIntegrationConfig } from '@/lib/integrations/environment';
-import { isLocale, localizedPath, type Locale } from '@/lib/localization/config';
-import { getMessages } from '@/lib/localization/messages';
-import { buildLocalizedMetadata } from '@/lib/metadata/build-metadata';
-import type { Metadata } from 'next';
-import { notFound } from 'next/navigation';
-import styles from './page.module.css';
-
-interface LocalePageProps {
- params: Promise<{ locale: string }>;
-}
-
-export async function generateMetadata({ params }: LocalePageProps): Promise {
- const { locale: localeValue } = await params;
- if (!isLocale(localeValue)) notFound();
- const messages = getMessages(localeValue);
- return buildLocalizedMetadata({
- locale: localeValue,
- routeId: 'home',
- title: messages.homepage.meta.title,
- description: messages.homepage.meta.description,
- indexable: true,
- });
-}
-
-export default async function LocaleHomePage({ params }: LocalePageProps) {
- const { locale: localeValue } = await params;
- if (!isLocale(localeValue)) notFound();
- const locale: Locale = localeValue;
- const messages = getMessages(locale);
- const content = buildHomepageContent(messages.homepage, messages.shell);
- const integrationConfig = getPublicIntegrationConfig();
- const homePath = localizedPath('home', locale);
-
- return (
-
-
-
-
-
-
{content.hero.eyebrow}
-
- {content.hero.title}
-
-
{content.hero.body}
-
-
-
- {content.hero.secondary.label}
-
-
-
-
-
-
-
-
-
-
-
-
-
- {content.trust.items.map((item) => (
-
- ))}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {content.roles.items.map((item) => (
-
- ))}
-
-
-
-
-
-
-
-
-
-
- {content.modules.items.map((item) => (
-
- ))}
-
-
-
-
-
-
-
-
-
-
- {content.results.metrics.map((metric) => (
-
- ))}
-
-
-
- {content.results.method.map((item) => (
- - {item}
- ))}
-
-
-
-
-
-
-
-
-
-
-
-
- {content.integrations.items.map((item) => (
- -
-
- {item}
-
- ))}
-
-
-
-
-
-
-
-
-
-
-
- {content.security.checks.map((check) => (
-
- ))}
-
-
-
-
-
-
-
-
-
-
-
- {content.pricing.factors.map((factor) => (
- -
-
- {factor}
-
- ))}
-
-
-
-
-
-
-
-
-
-
-
- ({
- id: item.id,
- title: item.title,
- content: {item.body},
- open: index === 0,
- }))}
- />
-
-
-
-
-
-
- );
-}
diff --git a/homepage/src/app/api/demo/route.ts b/homepage/src/app/api/demo/route.ts
deleted file mode 100644
index 47d0182..0000000
--- a/homepage/src/app/api/demo/route.ts
+++ /dev/null
@@ -1,159 +0,0 @@
-import { randomUUID } from 'node:crypto';
-import {
- demoSubmissionEnvelopeSchema,
- zodFieldErrors,
- type DemoSubmissionResult,
-} from '@/lib/demo/schema';
-import { submitDemoLead } from '@/lib/demo/service';
-import { getIntegrationEnvironment } from '@/lib/integrations/environment';
-import { isApplicationJson } from '@/lib/security/content-type';
-import { NextResponse } from 'next/server';
-
-const maximumBodyBytes = 16_384;
-const responseHeaders = { 'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff' };
-
-function failure(
- status: number,
- result: Extract,
-): NextResponse {
- return NextResponse.json(result, { status, headers: responseHeaders });
-}
-
-function isAllowedOrigin(
- request: Request,
- approvedOrigin: string | undefined,
- production: boolean,
-) {
- const origin = request.headers.get('origin');
- if (!origin) return !production;
- const requestOrigin = new URL(request.url).origin;
- if (origin === requestOrigin) return true;
- return approvedOrigin ? origin === new URL(approvedOrigin).origin : false;
-}
-
-export async function POST(request: Request): Promise> {
- const correlationId = randomUUID();
- let environment;
- try {
- environment = getIntegrationEnvironment();
- } catch {
- return failure(503, {
- ok: false,
- category: 'configuration',
- formErrorCode: 'integration_environment_invalid',
- retryable: false,
- correlationId,
- });
- }
-
- if (request.headers.get('x-rdg-form') !== 'demo-v1') {
- return failure(403, {
- ok: false,
- category: 'validation',
- formErrorCode: 'invalid_request_marker',
- retryable: false,
- correlationId,
- });
- }
-
- if (!isAllowedOrigin(request, environment.SITE_ORIGIN, environment.NODE_ENV === 'production')) {
- return failure(403, {
- ok: false,
- category: 'validation',
- formErrorCode: 'invalid_origin',
- retryable: false,
- correlationId,
- });
- }
-
- const contentType = request.headers.get('content-type');
- if (!isApplicationJson(contentType)) {
- return failure(415, {
- ok: false,
- category: 'validation',
- formErrorCode: 'unsupported_content_type',
- retryable: false,
- correlationId,
- });
- }
-
- const declaredLength = Number(request.headers.get('content-length') ?? '0');
- if (Number.isFinite(declaredLength) && declaredLength > maximumBodyBytes) {
- return failure(413, {
- ok: false,
- category: 'validation',
- formErrorCode: 'request_too_large',
- retryable: false,
- correlationId,
- });
- }
-
- let body: unknown;
- try {
- const raw = await request.text();
- if (new TextEncoder().encode(raw).byteLength > maximumBodyBytes) {
- return failure(413, {
- ok: false,
- category: 'validation',
- formErrorCode: 'request_too_large',
- retryable: false,
- correlationId,
- });
- }
- body = JSON.parse(raw);
- } catch {
- return failure(400, {
- ok: false,
- category: 'validation',
- formErrorCode: 'malformed_json',
- retryable: false,
- correlationId,
- });
- }
-
- const parsed = demoSubmissionEnvelopeSchema.safeParse(body);
- if (!parsed.success) {
- return failure(400, {
- ok: false,
- category: 'validation',
- fieldErrors: zodFieldErrors(parsed.error),
- formErrorCode: 'validation_failed',
- retryable: false,
- correlationId,
- });
- }
-
- if (parsed.data.guard.honeypot.length > 0) {
- return failure(400, {
- ok: false,
- category: 'abuse',
- formErrorCode: 'request_rejected',
- retryable: false,
- correlationId,
- });
- }
-
- const scenarioHeader = request.headers.get('x-rdg-test-scenario');
- const testScenario =
- environment.NODE_ENV !== 'production' &&
- environment.DEMO_TEST_SCENARIOS_ENABLED &&
- ['success', 'duplicate', 'timeout', 'integration-error'].includes(scenarioHeader ?? '')
- ? (scenarioHeader as 'success' | 'duplicate' | 'timeout' | 'integration-error')
- : undefined;
-
- const result = await submitDemoLead(parsed.data.lead, {
- environment,
- ...(testScenario ? { testScenario } : {}),
- });
-
- const status = result.ok
- ? 202
- : result.category === 'timeout'
- ? 504
- : result.category === 'configuration'
- ? 503
- : result.category === 'duplicate'
- ? 409
- : 502;
- return NextResponse.json(result, { status, headers: responseHeaders });
-}
diff --git a/homepage/src/app/robots.ts b/homepage/src/app/robots.ts
deleted file mode 100644
index 2eb6cfc..0000000
--- a/homepage/src/app/robots.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import type { MetadataRoute } from 'next';
-import { getSiteOrigin } from '@/lib/localization/site-origin';
-
-export default function robots(): MetadataRoute.Robots {
- const origin = getSiteOrigin();
- const approved = process.env.PUBLIC_RELEASE_APPROVED === 'true';
- return {
- rules: approved ? { userAgent: '*', allow: '/' } : { userAgent: '*', disallow: '/' },
- host: origin.href,
- };
-}
diff --git a/homepage/src/components/actions/ActionLink.module.css b/homepage/src/components/actions/ActionLink.module.css
deleted file mode 100644
index eba1e5d..0000000
--- a/homepage/src/components/actions/ActionLink.module.css
+++ /dev/null
@@ -1,59 +0,0 @@
-.link {
- display: inline-flex;
- min-block-size: 2.75rem;
- align-items: center;
- gap: var(--space-2);
- font-weight: 650;
- text-underline-offset: 0.18em;
-}
-.inline {
- min-block-size: auto;
- font-weight: inherit;
- text-decoration-thickness: 0.08em;
-}
-.navigation {
- color: var(--text-primary);
- text-decoration: none;
-}
-.navigation:hover {
- color: var(--link-hover);
- text-decoration: underline;
-}
-.standalone {
- text-decoration: none;
-}
-.standalone:hover {
- text-decoration: underline;
-}
-.button-primary,
-.button-conversion {
- justify-content: center;
- padding-block: var(--space-3);
- padding-inline: var(--space-5);
- border-radius: var(--radius-sm);
- color: var(--text-inverse);
- text-decoration: none;
-}
-.button-primary {
- background: var(--interactive-primary);
-}
-.button-primary:hover {
- background: var(--interactive-primary-hover);
- color: var(--text-inverse);
-}
-.button-conversion {
- background: var(--interactive-conversion);
-}
-.button-conversion:hover {
- background: var(--interactive-conversion-hover);
- color: var(--text-inverse);
-}
-.muted {
- color: var(--text-secondary);
- font-size: var(--type-label-size);
-}
-.disabled {
- cursor: not-allowed;
- color: var(--control-disabled-text);
- text-decoration: none;
-}
diff --git a/homepage/src/components/actions/ActionLink.tsx b/homepage/src/components/actions/ActionLink.tsx
deleted file mode 100644
index 982c0bd..0000000
--- a/homepage/src/components/actions/ActionLink.tsx
+++ /dev/null
@@ -1,76 +0,0 @@
-import { Icon, type ApprovedIconName } from '@/components/foundation/Icon';
-import { classNames } from '@/lib/components/classNames';
-import type { AnchorHTMLAttributes, ReactNode } from 'react';
-import styles from './ActionLink.module.css';
-
-type LinkVariant =
- | 'inline'
- | 'navigation'
- | 'standalone'
- | 'button-primary'
- | 'button-conversion'
- | 'muted';
-
-interface ActionLinkProps extends Omit, 'href'> {
- href: string;
- children: ReactNode;
- variant?: LinkVariant;
- icon?: ApprovedIconName;
- external?: boolean;
- newTab?: boolean;
- disabledReason?: string;
-}
-
-export function ActionLink({
- href,
- children,
- variant = 'inline',
- icon,
- external = false,
- newTab = false,
- disabledReason,
- className,
- ...props
-}: ActionLinkProps) {
- if (disabledReason) {
- return (
-
- {children}
- : {disabledReason}
-
- );
- }
- const isExternal = external || /^https?:\/\//.test(href);
- const trailingIcon = icon ?? (isExternal ? 'external' : undefined);
- const content = (
- <>
- {children}
- {trailingIcon ? (
-
- ) : null}
- {newTab ? (opens in a new tab) : null}
- >
- );
- const shared = {
- className: classNames(styles.link, styles[variant], className),
- ...(newTab ? { target: '_blank', rel: 'noopener noreferrer' } : {}),
- ...props,
- };
- return (
-
- {content}
-
- );
-}
diff --git a/homepage/src/components/actions/Button.module.css b/homepage/src/components/actions/Button.module.css
deleted file mode 100644
index 43187c3..0000000
--- a/homepage/src/components/actions/Button.module.css
+++ /dev/null
@@ -1,139 +0,0 @@
-.button {
- display: inline-flex;
- min-block-size: 2.75rem;
- align-items: center;
- justify-content: center;
- gap: var(--space-2);
- border: 1px solid transparent;
- border-radius: var(--radius-sm);
- cursor: pointer;
- font-weight: 750;
- line-height: 1.25;
- text-align: center;
- text-decoration: none;
- transition:
- background-color var(--duration-fast) var(--easing-productive),
- border-color var(--duration-fast) var(--easing-productive),
- color var(--duration-fast) var(--easing-productive),
- transform var(--duration-fast) var(--easing-productive);
-}
-.button:active:not(:disabled) {
- transform: translateY(1px);
-}
-.button:disabled {
- cursor: not-allowed;
- opacity: var(--opacity-disabled);
-}
-.small {
- min-block-size: 2.5rem;
- padding-block: var(--space-2);
- padding-inline: var(--space-3);
- font-size: var(--type-label-size);
-}
-.medium {
- padding-block: var(--space-3);
- padding-inline: var(--space-5);
-}
-.large {
- min-block-size: 3.25rem;
- padding-block: var(--space-3);
- padding-inline: var(--space-6);
- font-size: var(--type-body-large-size);
-}
-.primary {
- background: var(--interactive-primary);
- color: var(--text-inverse);
-}
-.primary:hover:not(:disabled) {
- background: var(--interactive-primary-hover);
-}
-.conversion {
- background: var(--interactive-conversion);
- color: var(--text-inverse);
-}
-.conversion:hover:not(:disabled) {
- background: var(--interactive-conversion-hover);
-}
-.secondary {
- border-color: var(--interactive-primary);
- background: var(--surface-primary);
- color: var(--interactive-primary);
-}
-.secondary:hover:not(:disabled) {
- background: var(--status-info-surface);
-}
-.tertiary {
- border-color: var(--border-standard);
- background: var(--surface-elevated);
- color: var(--text-primary);
-}
-.tertiary:hover:not(:disabled) {
- border-color: var(--border-strong);
- background: var(--surface-muted);
-}
-.ghost {
- background: transparent;
- color: var(--text-primary);
-}
-.ghost:hover:not(:disabled) {
- background: var(--surface-muted);
-}
-.destructive {
- background: var(--interactive-danger);
- color: var(--text-inverse);
-}
-.destructive:hover:not(:disabled) {
- filter: brightness(0.92);
-}
-.link {
- min-block-size: 2.75rem;
- padding-inline: 0;
- background: transparent;
- color: var(--link-default);
- text-decoration: underline;
- text-underline-offset: 0.18em;
-}
-.link:hover:not(:disabled) {
- color: var(--link-hover);
-}
-.iconOnly {
- aspect-ratio: 1;
- padding: var(--space-3);
-}
-.fullWidth {
- inline-size: 100%;
-}
-.loading {
- position: relative;
-}
-.spinner {
- position: absolute;
- animation: spin 0.9s linear infinite;
-}
-.loadingContent {
- display: inline-flex;
- align-items: center;
- gap: var(--space-2);
- opacity: 0;
-}
-.label {
- min-inline-size: 0;
-}
-@keyframes spin {
- to {
- transform: rotate(360deg);
- }
-}
-@media (prefers-reduced-motion: reduce) {
- .button {
- transition: none;
- }
- .spinner {
- animation-duration: 1.8s;
- }
-}
-@media (forced-colors: active) {
- .button {
- border-color: ButtonText;
- }
-}
diff --git a/homepage/src/components/actions/Button.tsx b/homepage/src/components/actions/Button.tsx
deleted file mode 100644
index 126bf14..0000000
--- a/homepage/src/components/actions/Button.tsx
+++ /dev/null
@@ -1,97 +0,0 @@
-'use client';
-
-import { Icon, type ApprovedIconName } from '@/components/foundation/Icon';
-import { classNames } from '@/lib/components/classNames';
-import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from 'react';
-import styles from './Button.module.css';
-
-export type ButtonIntent =
- | 'primary'
- | 'conversion'
- | 'secondary'
- | 'tertiary'
- | 'ghost'
- | 'destructive'
- | 'link';
-export type ButtonSize = 'small' | 'medium' | 'large';
-
-interface CommonButtonProps extends Omit, 'children'> {
- intent?: ButtonIntent;
- size?: ButtonSize;
- loading?: boolean;
- fullWidth?: boolean;
- leadingIcon?: ApprovedIconName;
- trailingIcon?: ApprovedIconName;
- children: ReactNode;
-}
-
-interface IconOnlyButtonProps extends Omit<
- CommonButtonProps,
- 'children' | 'leadingIcon' | 'trailingIcon'
-> {
- icon: ApprovedIconName;
- 'aria-label': string;
- children?: never;
-}
-
-export type ButtonProps = CommonButtonProps | IconOnlyButtonProps;
-
-export const Button = forwardRef(function Button(props, ref) {
- const {
- intent = 'primary',
- size = 'medium',
- loading = false,
- fullWidth = false,
- disabled = false,
- className,
- type = 'button',
- ...rest
- } = props;
- const iconOnly = 'icon' in props;
- const content = iconOnly ? (
-
- ) : (
- <>
- {props.leadingIcon ? : null}
- {props.children}
- {props.trailingIcon ? (
-
- ) : null}
- >
- );
-
- const nativeProps = { ...rest } as ButtonHTMLAttributes;
- delete (nativeProps as Partial).leadingIcon;
- delete (nativeProps as Partial).trailingIcon;
- delete (nativeProps as Partial).icon;
-
- return (
-
- );
-});
diff --git a/homepage/src/components/app-shell/BrandLink.tsx b/homepage/src/components/app-shell/BrandLink.tsx
deleted file mode 100644
index 9dae94f..0000000
--- a/homepage/src/components/app-shell/BrandLink.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-'use client';
-
-import { localizedPath, routeIdFromPathname, type Locale } from '@/lib/localization/config';
-import Image from 'next/image';
-import { usePathname } from 'next/navigation';
-import styles from './SiteHeader.module.css';
-
-export function BrandLink({ locale, label }: { locale: Locale; label: string }) {
- const pathname = usePathname();
- return (
-
-
- RentalDriveGo
-
- );
-}
diff --git a/homepage/src/components/app-shell/Controls.module.css b/homepage/src/components/app-shell/Controls.module.css
deleted file mode 100644
index c03a3b0..0000000
--- a/homepage/src/components/app-shell/Controls.module.css
+++ /dev/null
@@ -1,38 +0,0 @@
-.field {
- display: grid;
- gap: var(--space-1);
-}
-
-.label {
- color: var(--text-secondary);
- font-size: var(--type-caption-size);
- font-weight: 700;
-}
-
-.select {
- min-block-size: var(--size-touch-min);
- min-inline-size: 7.25rem;
- max-inline-size: 100%;
- border: 1px solid var(--border-strong);
- border-radius: var(--radius-sm);
- padding-block: var(--space-2);
- padding-inline: var(--space-3) var(--space-8);
- background: var(--surface-elevated);
- color: var(--text-primary);
-}
-
-.select:hover {
- border-color: var(--interactive-primary);
-}
-
-.compactLabel {
- position: absolute;
- inline-size: 1px;
- block-size: 1px;
- margin: -1px;
- padding: 0;
- overflow: hidden;
- border: 0;
- clip-path: inset(50%);
- white-space: nowrap;
-}
diff --git a/homepage/src/components/app-shell/DisabledAction.tsx b/homepage/src/components/app-shell/DisabledAction.tsx
deleted file mode 100644
index 8650f92..0000000
--- a/homepage/src/components/app-shell/DisabledAction.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import { StatusBadge } from '@/components/foundation/StatusBadge';
-import styles from './SiteHeader.module.css';
-
-interface DisabledActionProps {
- label: string;
- reason: string;
- pendingLabel?: string;
- conversion?: boolean;
-}
-
-export function DisabledAction({
- label,
- reason,
- pendingLabel,
- conversion = false,
-}: DisabledActionProps) {
- return (
-
- {label}
- {pendingLabel ? {pendingLabel} : null}
-
- );
-}
diff --git a/homepage/src/components/app-shell/HeaderNavigation.tsx b/homepage/src/components/app-shell/HeaderNavigation.tsx
deleted file mode 100644
index 347851d..0000000
--- a/homepage/src/components/app-shell/HeaderNavigation.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-'use client';
-
-import { localizedPath, routeIdFromPathname, type Locale } from '@/lib/localization/config';
-import { usePathname } from 'next/navigation';
-import { useEffect, useState } from 'react';
-import styles from './SiteHeader.module.css';
-
-export type HeaderNavId = 'product' | 'workflow' | 'modules' | 'pricing' | 'faq';
-
-interface HeaderNavigationProps {
- locale: Locale;
- label: string;
- labels: Record;
- onNavigate?: () => void;
- mobile?: boolean;
-}
-
-const navigationIds: HeaderNavId[] = ['product', 'workflow', 'modules', 'pricing', 'faq'];
-
-export function HeaderNavigation({
- locale,
- label,
- labels,
- onNavigate,
- mobile = false,
-}: HeaderNavigationProps) {
- const pathname = usePathname();
- const currentRoute = routeIdFromPathname(pathname);
- const [currentHash, setCurrentHash] = useState('');
-
- useEffect(() => {
- const updateHash = () => setCurrentHash(window.location.hash.replace(/^#/, ''));
- updateHash();
- window.addEventListener('hashchange', updateHash);
- return () => window.removeEventListener('hashchange', updateHash);
- }, []);
-
- const home = localizedPath('home', locale);
-
- return (
-
- );
-}
diff --git a/homepage/src/components/app-shell/LocaleSelector.tsx b/homepage/src/components/app-shell/LocaleSelector.tsx
deleted file mode 100644
index 197d2e5..0000000
--- a/homepage/src/components/app-shell/LocaleSelector.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-'use client';
-
-import { persistLocale } from '@/lib/localization/client';
-import {
- localeSwitchUrl,
- locales,
- routeIdFromPathname,
- type Locale,
-} from '@/lib/localization/config';
-import { usePathname } from 'next/navigation';
-import styles from './Controls.module.css';
-
-interface LocaleSelectorProps {
- currentLocale: Locale;
- label: string;
- localeNames: Record;
- compact?: boolean;
-}
-
-export function LocaleSelector({
- currentLocale,
- label,
- localeNames,
- compact = false,
-}: LocaleSelectorProps) {
- const pathname = usePathname();
- const routeId = routeIdFromPathname(pathname) ?? 'home';
-
- return (
-
- );
-}
diff --git a/homepage/src/components/app-shell/LocalizedLoading.tsx b/homepage/src/components/app-shell/LocalizedLoading.tsx
deleted file mode 100644
index 5939538..0000000
--- a/homepage/src/components/app-shell/LocalizedLoading.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-'use client';
-
-import { getClientShellMessages } from '@/lib/localization/client-messages';
-import { useParams } from 'next/navigation';
-import { LoadingSkeleton, StateShell } from './StateShell';
-
-export function LocalizedLoading() {
- const params = useParams<{ locale?: string }>();
- const messages = getClientShellMessages(params?.locale);
- return (
-
-
-
- );
-}
diff --git a/homepage/src/components/app-shell/LocalizedNotFound.tsx b/homepage/src/components/app-shell/LocalizedNotFound.tsx
deleted file mode 100644
index 9c74d22..0000000
--- a/homepage/src/components/app-shell/LocalizedNotFound.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-'use client';
-
-import { getClientShellMessages } from '@/lib/localization/client-messages';
-import { isLocale, localizedPath } from '@/lib/localization/config';
-import { usePathname } from 'next/navigation';
-import { StateAction, StateActions, StateShell } from './StateShell';
-
-export function LocalizedNotFound() {
- const pathname = usePathname();
- const localeValue = pathname.split('/').filter(Boolean)[0];
- const locale = isLocale(localeValue) ? localeValue : 'en';
- const messages = getClientShellMessages(locale);
- return (
-
-
- {messages.states.backHome}
-
-
- );
-}
diff --git a/homepage/src/components/app-shell/MobileNavigation.tsx b/homepage/src/components/app-shell/MobileNavigation.tsx
deleted file mode 100644
index eea8f58..0000000
--- a/homepage/src/components/app-shell/MobileNavigation.tsx
+++ /dev/null
@@ -1,146 +0,0 @@
-'use client';
-
-import { ActionLink } from '@/components/actions/ActionLink';
-import { DemoTrigger } from '@/components/integrations/DemoTrigger';
-import type { ShellMessages } from '@/lib/localization/messages';
-import { localizedPath, type Locale } from '@/lib/localization/config';
-import type { ThemePreference } from '@/lib/theme/config';
-import { useEffect, useRef, useState } from 'react';
-import { HeaderNavigation } from './HeaderNavigation';
-import { LocaleSelector } from './LocaleSelector';
-import styles from './SiteHeader.module.css';
-import { ThemeSelector } from './ThemeSelector';
-
-function signInUrl(locale: Locale): string {
- return localizedPath('sign-in', locale);
-}
-
-interface MobileNavigationProps {
- locale: Locale;
- messages: ShellMessages;
- themePreference: ThemePreference;
- demoEnabled: boolean;
-}
-
-export function MobileNavigation({
- locale,
- messages,
- themePreference,
- demoEnabled,
-}: MobileNavigationProps) {
- const [open, setOpen] = useState(false);
- const dialogRef = useRef(null);
- const triggerRef = useRef(null);
- const closeRef = useRef(null);
-
- useEffect(() => {
- const dialog = dialogRef.current;
- if (!dialog) return;
-
- if (open && !dialog.open) {
- dialog.showModal();
- document.body.dataset.navigationOpen = 'true';
- closeRef.current?.focus();
- } else if (!open && dialog.open) {
- dialog.close();
- }
-
- return () => {
- delete document.body.dataset.navigationOpen;
- };
- }, [open]);
-
- const close = () => {
- dialogRef.current?.close();
- setOpen(false);
- delete document.body.dataset.navigationOpen;
- window.requestAnimationFrame(() => triggerRef.current?.focus());
- };
-
- return (
-
-
-
-
-
- );
-}
diff --git a/homepage/src/components/app-shell/SiteFooter.module.css b/homepage/src/components/app-shell/SiteFooter.module.css
deleted file mode 100644
index 1ebad0a..0000000
--- a/homepage/src/components/app-shell/SiteFooter.module.css
+++ /dev/null
@@ -1,118 +0,0 @@
-.footer {
- border-block-start: 1px solid var(--border-standard);
- background: var(--surface-primary);
-}
-
-.inner {
- display: grid;
- inline-size: min(100%, var(--container-wide));
- margin-inline: auto;
- padding-block: var(--space-12) var(--space-6);
- padding-inline: var(--layout-gutter);
- gap: var(--space-10);
-}
-
-.brandColumn {
- display: grid;
- align-content: start;
- gap: var(--space-4);
- max-inline-size: 32rem;
-}
-
-.brand {
- display: inline-flex;
- align-items: center;
- gap: var(--space-3);
- color: var(--text-primary);
- font-weight: 800;
-}
-
-.mark {
- display: inline-grid;
- min-inline-size: var(--size-touch-min);
- min-block-size: var(--size-touch-min);
- padding-inline: var(--space-2);
- place-items: center;
- border-radius: var(--radius-sm);
- background: var(--interactive-primary);
- color: var(--text-inverse);
- font-size: var(--type-caption-size);
-}
-
-.tagline,
-.releaseNotice {
- margin: 0;
- color: var(--text-secondary);
-}
-
-.groups {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(min(100%, 11rem), 1fr));
- gap: var(--space-8);
-}
-
-.group h2 {
- margin-block-end: var(--space-3);
- font-size: var(--type-label-size);
- letter-spacing: var(--tracking-label);
- text-transform: uppercase;
-}
-
-html[dir='rtl'] .group h2 {
- letter-spacing: normal;
- text-transform: none;
-}
-
-.group ul {
- display: grid;
- margin: 0;
- padding: 0;
- gap: var(--space-2);
- list-style: none;
-}
-
-.group a,
-.pendingItem {
- display: inline-flex;
- min-block-size: var(--size-touch-min);
- align-items: center;
- gap: var(--space-2);
- color: var(--text-secondary);
- text-decoration: none;
-}
-
-.group a:hover {
- color: var(--interactive-primary);
- text-decoration: underline;
-}
-
-.pendingItem {
- flex-wrap: wrap;
- opacity: var(--opacity-disabled);
-}
-
-.legalLink {
- flex-wrap: wrap;
-}
-
-.bottom {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- justify-content: space-between;
- gap: var(--space-4);
- padding-block-start: var(--space-6);
- border-block-start: 1px solid var(--border-standard);
- color: var(--text-subdued);
- font-size: var(--type-caption-size);
-}
-
-@media (min-width: 768px) {
- .inner {
- grid-template-columns: minmax(14rem, 0.8fr) minmax(0, 2fr);
- }
-
- .bottom {
- grid-column: 1 / -1;
- }
-}
diff --git a/homepage/src/components/app-shell/SiteFooter.tsx b/homepage/src/components/app-shell/SiteFooter.tsx
deleted file mode 100644
index 213b7f8..0000000
--- a/homepage/src/components/app-shell/SiteFooter.tsx
+++ /dev/null
@@ -1,161 +0,0 @@
-import { StatusBadge } from '@/components/foundation/StatusBadge';
-import { DemoTrigger } from '@/components/integrations/DemoTrigger';
-import { localizedPath, type Locale } from '@/lib/localization/config';
-import type { ShellMessages } from '@/lib/localization/messages';
-import styles from './SiteFooter.module.css';
-
-function signInUrl(locale: Locale): string {
- return localizedPath('sign-in', locale);
-}
-
-interface SiteFooterProps {
- locale: Locale;
- messages: ShellMessages;
- demoEnabled: boolean;
-}
-
-function HashLink({ locale, hash, children }: { locale: Locale; hash: string; children: string }) {
- return {children};
-}
-
-function PendingRouteLink({
- locale,
- routeId,
- label,
- pending,
-}: {
- locale: Locale;
- routeId: 'sign-in' | 'forgot-password' | 'privacy' | 'terms' | 'accessibility';
- label: string;
- pending: string;
-}) {
- return (
-
- {label}
- {pending}
-
- );
-}
-
-function PendingItem({ label, pending }: { label: string; pending: string }) {
- return (
-
- {label}
- {pending}
-
- );
-}
-
-export function SiteFooter({ locale, messages, demoEnabled }: SiteFooterProps) {
- const footer = messages.footer;
- const year = new Date().getUTCFullYear();
-
- return (
-
- );
-}
diff --git a/homepage/src/components/app-shell/SiteHeader.module.css b/homepage/src/components/app-shell/SiteHeader.module.css
deleted file mode 100644
index 9051b96..0000000
--- a/homepage/src/components/app-shell/SiteHeader.module.css
+++ /dev/null
@@ -1,287 +0,0 @@
-.header {
- position: sticky;
- z-index: var(--layer-sticky);
- inset-block-start: 0;
- border-block-end: 1px solid var(--border-standard);
- background: color-mix(in srgb, var(--surface-primary) 94%, transparent);
- box-shadow: var(--shadow-subtle);
- backdrop-filter: blur(var(--space-3));
-}
-
-.inner {
- display: grid;
- min-block-size: var(--header-min-block-size);
- inline-size: min(100%, var(--container-wide));
- margin-inline: auto;
- padding-inline: var(--layout-gutter);
- grid-template-columns: auto minmax(0, 1fr) auto;
- align-items: center;
- gap: var(--space-5);
-}
-
-.brand {
- display: inline-flex;
- min-block-size: var(--size-touch-min);
- align-items: center;
- gap: var(--space-3);
- color: var(--text-primary);
- font-weight: 800;
- text-decoration: none;
- white-space: nowrap;
-}
-
-.mark {
- inline-size: 36px;
- block-size: 36px;
- border-radius: var(--radius-sm);
- object-fit: cover;
- flex-shrink: 0;
-}
-
-
-.navigation ul,
-.drawerNavigation ul {
- display: flex;
- margin: 0;
- padding: 0;
- list-style: none;
-}
-
-.navigation ul {
- justify-content: center;
- gap: var(--space-1);
-}
-
-.navigation a,
-.drawerNavigation a {
- display: inline-flex;
- min-block-size: var(--size-touch-min);
- align-items: center;
- border-radius: var(--radius-sm);
- color: var(--text-primary);
- font-weight: 650;
- text-decoration: none;
-}
-
-.navigation a {
- padding-inline: var(--space-3);
-}
-
-.navigation a:hover,
-.navigation a[aria-current],
-.drawerNavigation a:hover,
-.drawerNavigation a[aria-current] {
- background: var(--surface-muted);
- color: var(--interactive-primary);
-}
-
-.desktopControls {
- display: flex;
- align-items: center;
- gap: var(--space-2);
-}
-
-.actionDisabled,
-.conversionDisabled {
- display: inline-flex;
- min-block-size: var(--size-touch-min);
- align-items: center;
- justify-content: center;
- gap: var(--space-2);
- border: 1px solid var(--border-strong);
- border-radius: var(--radius-sm);
- padding-block: var(--space-2);
- padding-inline: var(--space-3);
- background: var(--control-disabled-surface);
- color: var(--control-disabled-text);
- font-weight: 700;
- opacity: var(--opacity-disabled);
- white-space: nowrap;
-}
-
-.actionLink {
- display: inline-flex;
- min-block-size: var(--size-touch-min);
- align-items: center;
- justify-content: center;
- gap: var(--space-2);
- border: 1px solid var(--border-strong);
- border-radius: var(--radius-sm);
- padding-block: var(--space-2);
- padding-inline: var(--space-3);
- color: var(--text-primary);
- font-weight: 700;
- white-space: nowrap;
- text-decoration: none;
-}
-
-.actionLink:hover {
- background: var(--interactive-primary);
- color: var(--text-inverse);
- border-color: var(--interactive-primary);
-}
-
-.conversionDisabled {
- border-color: var(--interactive-conversion);
-}
-
-.mobileActions,
-.mobileNavigation {
- display: none;
-}
-
-.menuButton,
-.closeButton {
- display: inline-grid;
- min-inline-size: var(--size-touch-min);
- min-block-size: var(--size-touch-min);
- place-items: center;
- border: 1px solid var(--border-strong);
- border-radius: var(--radius-sm);
- background: var(--surface-elevated);
- color: var(--text-primary);
-}
-
-.menuIcon {
- display: grid;
- inline-size: var(--space-5);
- gap: var(--space-1);
-}
-
-.menuIcon span {
- display: block;
- block-size: 2px;
- border-radius: var(--radius-pill);
- background: currentColor;
-}
-
-.drawer {
- inline-size: min(88vw, 384px);
- max-inline-size: none;
- block-size: 100dvh;
- max-block-size: none;
- margin-block: 0;
- margin-inline: 0 auto;
- padding: 0;
- border: 0;
- background: transparent;
- color: var(--text-primary);
-}
-
-html[dir='rtl'] .drawer {
- margin-inline: auto 0;
-}
-
-.drawer::backdrop {
- background: var(--overlay-scrim);
-}
-
-.drawerPanel {
- display: flex;
- block-size: 100%;
- flex-direction: column;
- gap: var(--space-6);
- overflow-y: auto;
- padding: var(--space-5);
- background: var(--surface-elevated);
- box-shadow: var(--shadow-overlay);
-}
-
-.drawerHeader {
- display: flex;
- min-block-size: var(--size-touch-min);
- align-items: center;
- justify-content: space-between;
- gap: var(--space-4);
-}
-
-.closeButton {
- font-size: var(--type-heading-3-size);
- line-height: 1;
-}
-
-.drawerNavigation ul {
- flex-direction: column;
- gap: var(--space-1);
-}
-
-.drawerNavigation a {
- inline-size: 100%;
- padding-inline: var(--space-3);
-}
-
-.drawerControls,
-.drawerActions {
- display: grid;
- gap: var(--space-4);
-}
-
-.drawerActions {
- margin-block-start: auto;
-}
-
-@media (min-width: 1120px) and (max-width: 1279px) {
- .brandName {
- position: absolute;
- inline-size: 1px;
- block-size: 1px;
- margin: -1px;
- overflow: hidden;
- clip-path: inset(50%);
- white-space: nowrap;
- }
-}
-
-@media (max-width: 1119px) {
- .inner {
- grid-template-columns: minmax(0, 1fr) auto;
- }
-
- .desktopNavigation,
- .desktopControls {
- display: none;
- }
-
- .mobileActions,
- .mobileNavigation {
- display: flex;
- align-items: center;
- gap: var(--space-2);
- }
-}
-
-@media (max-width: 559px) {
- .brandName {
- position: absolute;
- inline-size: 1px;
- block-size: 1px;
- margin: -1px;
- overflow: hidden;
- clip-path: inset(50%);
- white-space: nowrap;
- }
-
- .mobileActions > .conversionDisabled {
- display: none;
- }
-}
-
-@media (forced-colors: active) {
- .header {
- backdrop-filter: none;
- }
-
- .mark,
- .actionDisabled,
- .conversionDisabled,
- .menuButton,
- .closeButton {
- border: 1px solid CanvasText;
- }
-}
-
-@media (max-width: 559px) {
- .mobileDemoTrigger {
- display: none;
- }
-}
diff --git a/homepage/src/components/app-shell/SiteHeader.tsx b/homepage/src/components/app-shell/SiteHeader.tsx
deleted file mode 100644
index 2d65917..0000000
--- a/homepage/src/components/app-shell/SiteHeader.tsx
+++ /dev/null
@@ -1,85 +0,0 @@
-import { localizedPath, type Locale } from '@/lib/localization/config';
-import type { ShellMessages } from '@/lib/localization/messages';
-import type { ThemePreference } from '@/lib/theme/config';
-import { ActionLink } from '@/components/actions/ActionLink';
-import { DemoTrigger } from '@/components/integrations/DemoTrigger';
-import { BrandLink } from './BrandLink';
-import { HeaderNavigation } from './HeaderNavigation';
-import { LocaleSelector } from './LocaleSelector';
-import { MobileNavigation } from './MobileNavigation';
-import styles from './SiteHeader.module.css';
-import { ThemeSelector } from './ThemeSelector';
-
-function signInUrl(locale: Locale): string {
- return localizedPath('sign-in', locale);
-}
-
-interface SiteHeaderProps {
- locale: Locale;
- messages: ShellMessages;
- themePreference: ThemePreference;
- demoEnabled: boolean;
-}
-
-export function SiteHeader({ locale, messages, themePreference, demoEnabled }: SiteHeaderProps) {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
- {messages.header.login}
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/homepage/src/components/app-shell/StateShell.module.css b/homepage/src/components/app-shell/StateShell.module.css
deleted file mode 100644
index e9cc77a..0000000
--- a/homepage/src/components/app-shell/StateShell.module.css
+++ /dev/null
@@ -1,104 +0,0 @@
-.main {
- display: grid;
- min-block-size: calc(100dvh - var(--header-min-block-size));
- place-items: center;
- padding-block: var(--section-space);
- padding-inline: var(--layout-gutter);
-}
-
-.panel {
- display: grid;
- inline-size: min(100%, var(--container-reading));
- gap: var(--space-5);
- padding: clamp(var(--space-6), 6vw, var(--space-12));
- border: 1px solid var(--border-standard);
- border-radius: var(--radius-lg);
- background: var(--surface-elevated);
- box-shadow: var(--shadow-card);
- text-align: start;
-}
-
-.panel h1 {
- margin: 0;
- font-size: var(--type-heading-1-size);
-}
-
-.panel p {
- margin: 0;
- color: var(--text-secondary);
- font-size: var(--type-body-large-size);
-}
-
-.actions {
- display: flex;
- flex-wrap: wrap;
- gap: var(--space-3);
-}
-
-.action {
- display: inline-flex;
- min-block-size: var(--size-touch-min);
- align-items: center;
- justify-content: center;
- border: 1px solid var(--interactive-primary);
- border-radius: var(--radius-sm);
- padding-block: var(--space-2);
- padding-inline: var(--space-4);
- background: var(--interactive-primary);
- color: var(--text-inverse);
- font-weight: 750;
- text-decoration: none;
-}
-
-.action:hover {
- background: var(--interactive-primary-hover);
- color: var(--text-inverse);
-}
-
-.skeleton {
- display: grid;
- gap: var(--space-3);
-}
-
-.skeleton span {
- display: block;
- block-size: var(--space-4);
- border-radius: var(--radius-pill);
- background: linear-gradient(
- 90deg,
- var(--skeleton-base),
- var(--skeleton-highlight),
- var(--skeleton-base)
- );
- background-size: 200% 100%;
- animation: skeleton-shift 1.4s var(--easing-standard) infinite;
-}
-
-.skeleton span:first-child {
- inline-size: 64%;
-}
-
-.skeleton span:last-child {
- inline-size: 82%;
-}
-
-@keyframes skeleton-shift {
- from {
- background-position: 100% 0;
- }
-
- to {
- background-position: -100% 0;
- }
-}
-
-@media (prefers-reduced-motion: reduce) {
- .skeleton span {
- animation: none;
- }
-}
-
-.content {
- display: grid;
- gap: var(--space-4);
-}
diff --git a/homepage/src/components/app-shell/StateShell.tsx b/homepage/src/components/app-shell/StateShell.tsx
deleted file mode 100644
index c542313..0000000
--- a/homepage/src/components/app-shell/StateShell.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import type { ComponentPropsWithoutRef, ReactNode } from 'react';
-import styles from './StateShell.module.css';
-
-interface StateShellProps {
- title: string;
- body: string;
- children?: ReactNode;
- status?: boolean;
-}
-
-export function StateShell({ title, body, children, status = false }: StateShellProps) {
- return (
-
-
- {title}
- {body}
- {children ? {children}
: null}
-
-
- );
-}
-
-export function StateActions({ children }: { children: ReactNode }) {
- return {children}
;
-}
-
-export function StateAction({ children, ...props }: ComponentPropsWithoutRef<'a'>) {
- return (
-
- {children}
-
- );
-}
-
-export function StateButton({ children, ...props }: ComponentPropsWithoutRef<'button'>) {
- return (
-
- );
-}
-
-export function LoadingSkeleton() {
- return (
-
-
-
-
-
- );
-}
diff --git a/homepage/src/components/app-shell/ThemeController.tsx b/homepage/src/components/app-shell/ThemeController.tsx
deleted file mode 100644
index f801100..0000000
--- a/homepage/src/components/app-shell/ThemeController.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-'use client';
-
-import { applyTheme, currentThemePreference } from '@/lib/theme/client';
-import { themeStorageKey } from '@/lib/theme/config';
-import { useEffect } from 'react';
-
-export function ThemeController() {
- useEffect(() => {
- const media = window.matchMedia('(prefers-color-scheme: dark)');
- const handleSystemChange = () => {
- if (currentThemePreference() === 'system') applyTheme('system');
- };
- const handleStorage = (event: StorageEvent) => {
- if (event.key === themeStorageKey && event.newValue) {
- const preference = event.newValue;
- if (preference === 'light' || preference === 'dark' || preference === 'system') {
- applyTheme(preference);
- }
- }
- };
-
- media.addEventListener('change', handleSystemChange);
- window.addEventListener('storage', handleStorage);
- return () => {
- media.removeEventListener('change', handleSystemChange);
- window.removeEventListener('storage', handleStorage);
- };
- }, []);
-
- return null;
-}
diff --git a/homepage/src/components/app-shell/ThemeSelector.tsx b/homepage/src/components/app-shell/ThemeSelector.tsx
deleted file mode 100644
index 904dfdb..0000000
--- a/homepage/src/components/app-shell/ThemeSelector.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-'use client';
-
-import { persistTheme, themePreferenceEvent } from '@/lib/theme/client';
-import { themePreferences, themeStorageKey, type ThemePreference } from '@/lib/theme/config';
-import { useEffect, useId, useState } from 'react';
-import styles from './Controls.module.css';
-
-interface ThemeSelectorProps {
- initialPreference: ThemePreference;
- label: string;
- statusTemplate: string;
- optionLabels: Record;
- compact?: boolean;
-}
-
-export function ThemeSelector({
- initialPreference,
- label,
- statusTemplate,
- optionLabels,
- compact = false,
-}: ThemeSelectorProps) {
- const [preference, setPreference] = useState(initialPreference);
- const statusId = useId();
-
- useEffect(() => {
- const handleStorage = (event: StorageEvent) => {
- if (
- event.key === themeStorageKey &&
- (event.newValue === 'light' || event.newValue === 'dark' || event.newValue === 'system')
- ) {
- setPreference(event.newValue);
- }
- };
- const handlePreference = (event: Event) => {
- const preferenceEvent = event as CustomEvent;
- setPreference(preferenceEvent.detail);
- };
- window.addEventListener('storage', handleStorage);
- window.addEventListener(themePreferenceEvent, handlePreference);
- return () => {
- window.removeEventListener('storage', handleStorage);
- window.removeEventListener(themePreferenceEvent, handlePreference);
- };
- }, []);
-
- const status = statusTemplate.replace('{value}', optionLabels[preference]);
-
- return (
-
- );
-}
diff --git a/homepage/src/components/auth/AuthShell.tsx b/homepage/src/components/auth/AuthShell.tsx
deleted file mode 100644
index 328d01e..0000000
--- a/homepage/src/components/auth/AuthShell.tsx
+++ /dev/null
@@ -1,97 +0,0 @@
-'use client'
-
-import { localizedPath, type Locale } from '@/lib/localization/config'
-import Image from 'next/image'
-import type { ReactNode } from 'react'
-
-interface AuthShellProps {
- locale: Locale
- title: string
- subtitle?: string
- children: ReactNode
-}
-
-export function AuthShell({ locale, title, subtitle, children }: AuthShellProps) {
- return (
-
-
-
-
-
-
-
- RentalDriveGo
-
-
- {title}
-
- {subtitle ? (
-
- {subtitle}
-
- ) : null}
-
-
-
-
-
- )
-}
diff --git a/homepage/src/components/auth/ForgotPasswordForm.tsx b/homepage/src/components/auth/ForgotPasswordForm.tsx
deleted file mode 100644
index e61ab51..0000000
--- a/homepage/src/components/auth/ForgotPasswordForm.tsx
+++ /dev/null
@@ -1,189 +0,0 @@
-'use client'
-
-import { Button } from '@/components/actions/Button'
-import { TextInput } from '@/components/forms/TextInput'
-import { FormField } from '@/components/forms/FormField'
-import { AuthShell } from '@/components/auth/AuthShell'
-import { API_BASE } from '@/lib/api'
-import { localizedPath, type Locale } from '@/lib/localization/config'
-import { useState } from 'react'
-
-interface Dict {
- title: string
- subtitle: string
- email: string
- emailPlaceholder: string
- submit: string
- submitting: string
- backToLogin: string
- successTitle: string
- successBody: string
- error: string
-}
-
-const dicts: Record = {
- en: {
- title: 'Forgot your password?',
- subtitle: "Enter your work email and we'll send you a reset link.",
- email: 'Email',
- emailPlaceholder: 'owner@company.com',
- submit: 'Send reset link',
- submitting: 'Sending…',
- backToLogin: 'Back to sign in',
- successTitle: 'Check your email',
- successBody: 'If that email is registered, a reset link has been sent. It expires in 60 minutes.',
- error: 'Something went wrong. Please try again.',
- },
- fr: {
- title: 'Mot de passe oublié ?',
- subtitle: "Entrez votre email professionnel et nous vous enverrons un lien de réinitialisation.",
- email: 'Email',
- emailPlaceholder: 'owner@company.com',
- submit: 'Envoyer le lien',
- submitting: 'Envoi…',
- backToLogin: 'Retour à la connexion',
- successTitle: 'Vérifiez votre email',
- successBody: "Si cet email est enregistré, un lien de réinitialisation a été envoyé. Il expire dans 60 minutes.",
- error: 'Une erreur est survenue. Veuillez réessayer.',
- },
- ar: {
- title: 'نسيت كلمة المرور؟',
- subtitle: 'أدخل بريدك الإلكتروني وسنرسل لك رابط إعادة التعيين.',
- email: 'البريد الإلكتروني',
- emailPlaceholder: 'owner@company.com',
- submit: 'إرسال رابط إعادة التعيين',
- submitting: 'جارٍ الإرسال…',
- backToLogin: 'العودة إلى تسجيل الدخول',
- successTitle: 'تحقق من بريدك الإلكتروني',
- successBody: 'إذا كان البريد الإلكتروني مسجلاً، فقد تم إرسال رابط إعادة التعيين. تنتهي صلاحيته خلال 60 دقيقة.',
- error: 'حدث خطأ ما. يرجى المحاولة مرة أخرى.',
- },
-}
-
-export function ForgotPasswordForm({ locale }: { locale: Locale }) {
- const dict = (dicts[locale] ?? dicts.en) as Dict
- const [email, setEmail] = useState('')
- const [loading, setLoading] = useState(false)
- const [sent, setSent] = useState(false)
- const [error, setError] = useState(null)
-
- async function handleSubmit(e: React.FormEvent) {
- e.preventDefault()
- setLoading(true)
- setError(null)
- try {
- const [empRes, adminRes] = await Promise.all([
- fetch(`${API_BASE}/auth/employee/forgot-password`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ email }),
- }),
- fetch(`${API_BASE}/admin/auth/forgot-password`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ email }),
- }),
- ])
- if (!empRes.ok && !adminRes.ok) throw new Error()
- setSent(true)
- } catch {
- setError(dict.error)
- } finally {
- setLoading(false)
- }
- }
-
- const signInHref = localizedPath('sign-in', locale)
-
- return (
-
- {sent ? (
-
-
-
- {dict.successTitle}
-
-
- {dict.successBody}
-
-
- ) : (
-
- )}
-
-
-
- )
-}
diff --git a/homepage/src/components/auth/ResetPasswordForm.tsx b/homepage/src/components/auth/ResetPasswordForm.tsx
deleted file mode 100644
index 4104d31..0000000
--- a/homepage/src/components/auth/ResetPasswordForm.tsx
+++ /dev/null
@@ -1,293 +0,0 @@
-'use client'
-
-import { Button } from '@/components/actions/Button'
-import { FormField } from '@/components/forms/FormField'
-import { AuthShell } from '@/components/auth/AuthShell'
-import { API_BASE } from '@/lib/api'
-import { localizedPath, type Locale } from '@/lib/localization/config'
-import { useSearchParams } from 'next/navigation'
-import { Suspense, useState } from 'react'
-
-interface Dict {
- title: string
- subtitle: string
- newPassword: string
- confirmPassword: string
- submit: string
- submitting: string
- backToLogin: string
- successTitle: string
- successBody: string
- signIn: string
- errorMismatch: string
- errorShort: string
- errorInvalidToken: string
- errorGeneric: string
- noToken: string
- requestNew: string
-}
-
-const dicts: Record = {
- en: {
- title: 'Set new password',
- subtitle: 'Choose a strong password for your account.',
- newPassword: 'New password',
- confirmPassword: 'Confirm password',
- submit: 'Reset password',
- submitting: 'Resetting…',
- backToLogin: 'Back to sign in',
- successTitle: 'Password updated',
- successBody: 'Your password has been reset. You can now sign in with your new password.',
- signIn: 'Sign in',
- errorMismatch: 'Passwords do not match.',
- errorShort: 'Password must be at least 8 characters.',
- errorInvalidToken: 'This reset link is invalid or has expired. Please request a new one.',
- errorGeneric: 'Something went wrong. Please try again.',
- noToken: 'No reset token found. Please request a new password reset.',
- requestNew: 'Request new reset link',
- },
- fr: {
- title: 'Nouveau mot de passe',
- subtitle: 'Choisissez un mot de passe fort pour votre compte.',
- newPassword: 'Nouveau mot de passe',
- confirmPassword: 'Confirmer le mot de passe',
- submit: 'Réinitialiser',
- submitting: 'Traitement…',
- backToLogin: 'Retour à la connexion',
- successTitle: 'Mot de passe mis à jour',
- successBody: 'Votre mot de passe a été réinitialisé. Vous pouvez maintenant vous connecter.',
- signIn: 'Se connecter',
- errorMismatch: 'Les mots de passe ne correspondent pas.',
- errorShort: 'Le mot de passe doit contenir au moins 8 caractères.',
- errorInvalidToken: 'Ce lien est invalide ou a expiré. Veuillez en demander un nouveau.',
- errorGeneric: 'Une erreur est survenue. Veuillez réessayer.',
- noToken: 'Aucun jeton de réinitialisation trouvé. Veuillez demander un nouveau lien.',
- requestNew: 'Demander un nouveau lien',
- },
- ar: {
- title: 'تعيين كلمة مرور جديدة',
- subtitle: 'اختر كلمة مرور قوية لحسابك.',
- newPassword: 'كلمة المرور الجديدة',
- confirmPassword: 'تأكيد كلمة المرور',
- submit: 'إعادة تعيين',
- submitting: 'جارٍ المعالجة…',
- backToLogin: 'العودة إلى تسجيل الدخول',
- successTitle: 'تم تحديث كلمة المرور',
- successBody: 'تم إعادة تعيين كلمة المرور. يمكنك الآن تسجيل الدخول.',
- signIn: 'تسجيل الدخول',
- errorMismatch: 'كلمتا المرور غير متطابقتين.',
- errorShort: 'يجب أن تتكون كلمة المرور من 8 أحرف على الأقل.',
- errorInvalidToken: 'رابط إعادة التعيين غير صالح أو منتهي الصلاحية.',
- errorGeneric: 'حدث خطأ ما. يرجى المحاولة مرة أخرى.',
- noToken: 'لم يتم العثور على رمز إعادة التعيين. يرجى طلب رابط جديد.',
- requestNew: 'طلب رابط جديد',
- },
-}
-
-function ResetPasswordContent({ locale }: { locale: Locale }) {
- const dict = (dicts[locale] ?? dicts.en) as Dict
- const searchParams = useSearchParams()
- const token = searchParams.get('token')
-
- const [password, setPassword] = useState('')
- const [confirm, setConfirm] = useState('')
- const [loading, setLoading] = useState(false)
- const [done, setDone] = useState(false)
- const [error, setError] = useState(null)
-
- async function handleSubmit(e: React.FormEvent) {
- e.preventDefault()
- if (password.length < 8) { setError(dict.errorShort); return }
- if (password !== confirm) { setError(dict.errorMismatch); return }
- setLoading(true)
- setError(null)
- try {
- // Try employee endpoint first, fall back to admin
- let res = await fetch(`${API_BASE}/auth/employee/reset-password`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ token, password }),
- })
-
- if (!res.ok) {
- res = await fetch(`${API_BASE}/admin/auth/reset-password`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ token, password }),
- })
- }
-
- const json = await res.json()
- if (!res.ok) {
- if (json?.error === 'invalid_token') throw new Error(dict.errorInvalidToken)
- throw new Error(dict.errorGeneric)
- }
- setDone(true)
- } catch (err: any) {
- setError(err.message ?? dict.errorGeneric)
- } finally {
- setLoading(false)
- }
- }
-
- const signInHref = localizedPath('sign-in', locale)
- const forgotHref = localizedPath('forgot-password', locale)
-
- if (!token) {
- return (
-
-
- {dict.noToken}
-
-
-
- )
- }
-
- if (done) {
- return (
-
-
-
-
- {dict.successTitle}
-
-
- {dict.successBody}
-
-
-
-
- )
- }
-
- return (
-
-
-
-
-
- )
-}
-
-export function ResetPasswordForm({ locale }: { locale: Locale }) {
- return (
-
-
-
- )
-}
diff --git a/homepage/src/components/auth/SignInForm.tsx b/homepage/src/components/auth/SignInForm.tsx
deleted file mode 100644
index 2c7db3c..0000000
--- a/homepage/src/components/auth/SignInForm.tsx
+++ /dev/null
@@ -1,364 +0,0 @@
-'use client'
-
-import { Button } from '@/components/actions/Button'
-import { TextInput } from '@/components/forms/TextInput'
-import { FormField } from '@/components/forms/FormField'
-import { AuthShell } from '@/components/auth/AuthShell'
-import { API_BASE, EMPLOYEE_PROFILE_KEY } from '@/lib/api'
-import { localizedPath, type Locale } from '@/lib/localization/config'
-import { useSearchParams, useRouter } from 'next/navigation'
-import { useState } from 'react'
-
-interface Dict {
- title: string
- subtitle: string
- email: string
- emailPlaceholder: string
- password: string
- signIn: string
- signingIn: string
- verify: string
- verifying: string
- authCode: string
- enterCode: string
- totpPlaceholder: string
- back: string
- forgotPassword: string
- invalidCredentials: string
- passwordNotSet: string
- tooManyRequests: string
- emailNotVerified: string
- unexpectedError: string
-}
-
-const dicts: Record = {
- en: {
- title: 'Sign in',
- subtitle: 'Enter your credentials to access your account.',
- email: 'Email',
- emailPlaceholder: 'owner@company.com',
- password: 'Password',
- signIn: 'Sign in',
- signingIn: 'Signing in…',
- verify: 'Verify code',
- verifying: 'Verifying…',
- authCode: 'Authentication code',
- enterCode: 'Enter the 6-digit code from your authenticator app.',
- totpPlaceholder: '000000 or XXXX-XXXX-XXXX',
- back: 'Back to credentials',
- forgotPassword: 'Forgot your password?',
- invalidCredentials: 'Invalid email or password.',
- passwordNotSet: 'No password set yet. Check your invitation email or use "Forgot your password?"',
- tooManyRequests: 'Too many attempts. Please try again later.',
- emailNotVerified: 'Please verify your email first. Check your inbox for the verification link.',
- unexpectedError: 'Something went wrong. Please try again.',
- },
- fr: {
- title: 'Connexion',
- subtitle: 'Saisissez vos identifiants pour accéder à votre compte.',
- email: 'Email',
- emailPlaceholder: 'owner@company.com',
- password: 'Mot de passe',
- signIn: 'Connexion',
- signingIn: 'Connexion…',
- verify: 'Vérifier le code',
- verifying: 'Vérification…',
- authCode: "Code d'authentification",
- enterCode: "Entrez le code à 6 chiffres de votre application d'authentification.",
- totpPlaceholder: '000000 ou XXXX-XXXX-XXXX',
- back: 'Retour aux identifiants',
- forgotPassword: 'Mot de passe oublié ?',
- invalidCredentials: 'Adresse e-mail ou mot de passe invalide.',
- passwordNotSet: "Aucun mot de passe défini. Vérifiez votre e-mail d'invitation ou utilisez « Mot de passe oublié ? »",
- tooManyRequests: 'Trop de tentatives. Veuillez réessayer plus tard.',
- emailNotVerified: 'Veuillez vérifier votre adresse email. Consultez votre boîte de réception.',
- unexpectedError: 'Une erreur est survenue. Veuillez réessayer.',
- },
- ar: {
- title: 'تسجيل الدخول',
- subtitle: 'أدخل بياناتك للوصول إلى حسابك.',
- email: 'البريد الإلكتروني',
- emailPlaceholder: 'owner@company.com',
- password: 'كلمة المرور',
- signIn: 'تسجيل الدخول',
- signingIn: 'جارٍ تسجيل الدخول…',
- verify: 'تحقق من الرمز',
- verifying: 'جارٍ التحقق…',
- authCode: 'رمز المصادقة',
- enterCode: 'أدخل الرمز المكون من 6 أرقام من تطبيق المصادقة.',
- totpPlaceholder: '000000 أو XXXX-XXXX-XXXX',
- back: 'العودة إلى بيانات الدخول',
- forgotPassword: 'نسيت كلمة المرور؟',
- invalidCredentials: 'البريد الإلكتروني أو كلمة المرور غير صحيحة.',
- passwordNotSet: 'لم يتم تعيين كلمة مرور بعد. تحقق من بريد الدعوة أو استخدم "نسيت كلمة المرور؟"',
- tooManyRequests: 'محاولات كثيرة جدًا. يرجى المحاولة لاحقًا.',
- emailNotVerified: 'يرجى التحقق من بريدك الإلكتروني أولاً. تحقق من صندوق الوارد.',
- unexpectedError: 'حدث خطأ ما. يرجى المحاولة مرة أخرى.',
- },
-}
-
-export function SignInForm({ locale }: { locale: Locale }) {
- const dict = (dicts[locale] ?? dicts.en) as Dict
- const searchParams = useSearchParams()
- const [email, setEmail] = useState('')
- const [password, setPassword] = useState('')
- const [totpCode, setTotpCode] = useState('')
- const [step, setStep] = useState<'credentials' | 'totp'>('credentials')
- const [loading, setLoading] = useState(false)
- const [error, setError] = useState(null)
- const requestedPortal = searchParams.get('portal')
- const employeeRedirect = searchParams.get('redirect') || '/dashboard'
- const preferAdminAuth = requestedPortal === 'admin'
-
- async function handleCredentials(e: React.FormEvent) {
- e.preventDefault()
- setLoading(true)
- setError(null)
-
- try {
- const tryAdminLogin = async () => {
- const adminRes = await fetch(`${API_BASE}/admin/auth/login`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- credentials: 'include',
- body: JSON.stringify({ email, password }),
- })
- const adminJson = await adminRes.json()
-
- if (adminRes.ok && adminJson?.data?.admin) {
- window.location.href = `${window.location.origin}/admin/dashboard`
- return true
- }
-
- if (adminRes.status === 401 && adminJson?.error === 'totp_required') {
- setStep('totp')
- return true
- }
-
- if (adminRes.status === 429) {
- setError(dict.tooManyRequests)
- return true
- }
-
- return false
- }
-
- const tryEmployeeLogin = async () => {
- const empRes = await fetch(`${API_BASE}/auth/employee/login`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- credentials: 'include',
- body: JSON.stringify({ email, password }),
- })
- const empJson = await empRes.json()
-
- if (empRes.ok && empJson?.data?.employee) {
- if (empJson?.data?.employee) {
- localStorage.setItem(EMPLOYEE_PROFILE_KEY, JSON.stringify(empJson.data.employee))
- const prefLang = empJson.data.employee?.preferredLanguage
- if (prefLang === 'en' || prefLang === 'fr' || prefLang === 'ar') {
- document.cookie = `rentaldrivego-language=${prefLang}; path=/; max-age=31536000; samesite=lax`
- }
- }
- window.dispatchEvent(new CustomEvent('rentaldrivego:auth-changed'))
- window.location.replace(employeeRedirect)
- return true
- }
-
- if (empJson?.error === 'password_not_set') {
- setError(dict.passwordNotSet)
- return true
- }
-
- if (empJson?.error === 'email_not_verified') {
- setError(dict.emailNotVerified)
- return true
- }
-
- if (empRes.status === 429) {
- setError(dict.tooManyRequests)
- return true
- }
-
- return false
- }
-
- if (preferAdminAuth) {
- if (await tryAdminLogin()) return
- setError(dict.invalidCredentials)
- return
- }
-
- if (await tryEmployeeLogin()) return
-
- setError(dict.invalidCredentials)
- } catch {
- setError(dict.unexpectedError)
- } finally {
- setLoading(false)
- }
- }
-
- async function handleTotp(e: React.FormEvent) {
- e.preventDefault()
- setLoading(true)
- setError(null)
-
- try {
- const normalizedCode = totpCode.trim().toUpperCase()
- const secondFactor = /^\d{6}$/.test(normalizedCode)
- ? { totpCode: normalizedCode }
- : { recoveryCode: normalizedCode }
-
- const adminRes = await fetch(`${API_BASE}/admin/auth/login`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- credentials: 'include',
- body: JSON.stringify({ email, password, ...secondFactor }),
- })
- const adminJson = await adminRes.json()
-
- if (adminRes.ok && adminJson?.data?.admin) {
- window.location.href = `${window.location.origin}/admin/dashboard`
- return
- }
-
- setError(dict.invalidCredentials)
- } catch {
- setError(dict.unexpectedError)
- } finally {
- setLoading(false)
- }
- }
-
- const hrefBase = localizedPath('forgot-password', locale)
-
- return (
-
- {error ? (
-
- {error}
-
- ) : null}
-
- {step === 'credentials' ? (
-
- ) : (
-
- )}
-
- )
-}
diff --git a/homepage/src/components/controls/Accordion.module.css b/homepage/src/components/controls/Accordion.module.css
deleted file mode 100644
index 1ef9c16..0000000
--- a/homepage/src/components/controls/Accordion.module.css
+++ /dev/null
@@ -1,41 +0,0 @@
-.accordion {
- border-block-start: 1px solid var(--border-standard);
-}
-.item {
- border-block-end: 1px solid var(--border-standard);
-}
-.summary {
- display: flex;
- min-block-size: 3.5rem;
- align-items: center;
- justify-content: space-between;
- gap: var(--space-4);
- padding-block: var(--space-4);
- cursor: pointer;
- list-style: none;
-}
-.summary::-webkit-details-marker {
- display: none;
-}
-.heading {
- margin: 0;
- font-size: var(--type-body-large-size);
-}
-.icon {
- transition: transform var(--duration-fast) var(--easing-productive);
-}
-.item[open] .icon {
- transform: rotate(180deg);
-}
-.content {
- padding-block-end: var(--space-5);
- color: var(--text-secondary);
-}
-.content > :last-child {
- margin-block-end: 0;
-}
-@media (prefers-reduced-motion: reduce) {
- .icon {
- transition: none;
- }
-}
diff --git a/homepage/src/components/controls/Accordion.tsx b/homepage/src/components/controls/Accordion.tsx
deleted file mode 100644
index 8bff959..0000000
--- a/homepage/src/components/controls/Accordion.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import { Icon } from '@/components/foundation/Icon';
-import type { ReactNode } from 'react';
-import styles from './Accordion.module.css';
-
-export interface AccordionItem {
- id: string;
- title: string;
- content: ReactNode;
- open?: boolean;
-}
-
-export function Accordion({
- items,
- headingLevel = 3,
-}: {
- items: AccordionItem[];
- headingLevel?: 2 | 3 | 4;
-}) {
- const Heading = `h${headingLevel}` as const;
- return (
-
- {items.map((item) => (
-
-
- {item.title}
-
-
- {item.content}
-
- ))}
-
- );
-}
diff --git a/homepage/src/components/controls/Pagination.module.css b/homepage/src/components/controls/Pagination.module.css
deleted file mode 100644
index d829147..0000000
--- a/homepage/src/components/controls/Pagination.module.css
+++ /dev/null
@@ -1,33 +0,0 @@
-.pagination {
- display: grid;
- grid-template-columns: 1fr auto 1fr;
- align-items: center;
- gap: var(--space-4);
-}
-.pagination > :last-child {
- justify-self: end;
-}
-.pages {
- display: flex;
- flex-wrap: wrap;
- justify-content: center;
- gap: var(--space-1);
- margin: 0;
- padding: 0;
- list-style: none;
-}
-.page {
- min-inline-size: 2.75rem;
- justify-content: center;
- text-decoration: none;
-}
-.page[aria-current='page'] {
- border-radius: var(--radius-sm);
- background: var(--interactive-primary);
- color: var(--text-inverse);
-}
-@media (max-width: 479px) {
- .pages {
- display: none;
- }
-}
diff --git a/homepage/src/components/controls/Pagination.tsx b/homepage/src/components/controls/Pagination.tsx
deleted file mode 100644
index e9526d7..0000000
--- a/homepage/src/components/controls/Pagination.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import { ActionLink } from '@/components/actions/ActionLink';
-import { Icon } from '@/components/foundation/Icon';
-import styles from './Pagination.module.css';
-
-export interface PaginationPage {
- number: number;
- href: string;
- current?: boolean;
-}
-
-interface PaginationProps {
- label: string;
- pages: PaginationPage[];
- previous?: { href: string; label: string };
- next?: { href: string; label: string };
-}
-
-export function Pagination({ label, pages, previous, next }: PaginationProps) {
- return (
-
- );
-}
diff --git a/homepage/src/components/controls/SegmentedControl.module.css b/homepage/src/components/controls/SegmentedControl.module.css
deleted file mode 100644
index 101532f..0000000
--- a/homepage/src/components/controls/SegmentedControl.module.css
+++ /dev/null
@@ -1,31 +0,0 @@
-.control {
- display: inline-flex;
- max-inline-size: 100%;
- gap: var(--space-1);
- padding: var(--space-1);
- border: 1px solid var(--border-standard);
- border-radius: var(--radius-sm);
- background: var(--surface-muted);
- overflow-x: auto;
-}
-.option {
- flex: none;
- min-block-size: 2.5rem;
- padding-block: var(--space-2);
- padding-inline: var(--space-4);
- border: 0;
- border-radius: calc(var(--radius-sm) - 2px);
- background: transparent;
- color: var(--text-secondary);
- cursor: pointer;
- font-weight: 700;
-}
-.option[aria-checked='true'] {
- background: var(--surface-elevated);
- color: var(--text-primary);
- box-shadow: var(--shadow-subtle);
-}
-.option:disabled {
- cursor: not-allowed;
- opacity: var(--opacity-disabled);
-}
diff --git a/homepage/src/components/controls/SegmentedControl.tsx b/homepage/src/components/controls/SegmentedControl.tsx
deleted file mode 100644
index 3271d6c..0000000
--- a/homepage/src/components/controls/SegmentedControl.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-'use client';
-
-import { classNames } from '@/lib/components/classNames';
-import styles from './SegmentedControl.module.css';
-
-export interface SegmentOption {
- value: string;
- label: string;
- disabled?: boolean;
-}
-
-interface SegmentedControlProps {
- label: string;
- value: string;
- options: SegmentOption[];
- onChange: (value: string) => void;
- className?: string;
-}
-
-export function SegmentedControl({
- label,
- value,
- options,
- onChange,
- className,
-}: SegmentedControlProps) {
- return (
-
- {options.map((option) => (
-
- ))}
-
- );
-}
diff --git a/homepage/src/components/controls/Tabs.module.css b/homepage/src/components/controls/Tabs.module.css
deleted file mode 100644
index 47b5f78..0000000
--- a/homepage/src/components/controls/Tabs.module.css
+++ /dev/null
@@ -1,33 +0,0 @@
-.tabs {
- min-inline-size: 0;
-}
-.list {
- display: flex;
- gap: var(--space-1);
- overflow-x: auto;
- border-block-end: 1px solid var(--border-standard);
- scrollbar-width: thin;
-}
-.tab {
- flex: none;
- min-block-size: 2.75rem;
- padding-block: var(--space-3);
- padding-inline: var(--space-4);
- border: 0;
- border-block-end: 3px solid transparent;
- background: transparent;
- color: var(--text-secondary);
- cursor: pointer;
- font-weight: 700;
-}
-.tab[aria-selected='true'] {
- border-block-end-color: var(--interactive-primary);
- color: var(--text-primary);
-}
-.tab:disabled {
- cursor: not-allowed;
- opacity: var(--opacity-disabled);
-}
-.panel {
- padding-block: var(--space-5);
-}
diff --git a/homepage/src/components/controls/Tabs.tsx b/homepage/src/components/controls/Tabs.tsx
deleted file mode 100644
index eb2085a..0000000
--- a/homepage/src/components/controls/Tabs.tsx
+++ /dev/null
@@ -1,116 +0,0 @@
-'use client';
-
-import { classNames } from '@/lib/components/classNames';
-import { useId, useRef, useState, type KeyboardEvent, type ReactNode } from 'react';
-import styles from './Tabs.module.css';
-
-export interface TabItem {
- id: string;
- label: string;
- content: ReactNode;
- disabled?: boolean;
-}
-
-interface TabsProps {
- items: TabItem[];
- label: string;
- defaultActiveId?: string;
- activation?: 'automatic' | 'manual';
- className?: string;
-}
-
-export function Tabs({
- items,
- label,
- defaultActiveId,
- activation = 'automatic',
- className,
-}: TabsProps) {
- const available = items.filter((item) => !item.disabled);
- const initial =
- defaultActiveId && available.some((item) => item.id === defaultActiveId)
- ? defaultActiveId
- : (available[0]?.id ?? '');
- const [activeId, setActiveId] = useState(initial);
- const [focusedId, setFocusedId] = useState(initial);
- const refs = useRef(new Map());
- const prefix = useId().replaceAll(':', '');
-
- const focusByOffset = (currentId: string, offset: number) => {
- const currentIndex = available.findIndex((item) => item.id === currentId);
- if (currentIndex < 0 || available.length === 0) return;
- const next = available[(currentIndex + offset + available.length) % available.length];
- if (!next) return;
- setFocusedId(next.id);
- if (activation === 'automatic') setActiveId(next.id);
- refs.current.get(next.id)?.focus();
- };
-
- const handleKeyDown = (event: KeyboardEvent, id: string) => {
- if (event.key === 'ArrowRight') {
- event.preventDefault();
- focusByOffset(id, document.documentElement.dir === 'rtl' ? -1 : 1);
- } else if (event.key === 'ArrowLeft') {
- event.preventDefault();
- focusByOffset(id, document.documentElement.dir === 'rtl' ? 1 : -1);
- } else if (event.key === 'Home') {
- event.preventDefault();
- const first = available[0];
- if (first) focusByOffset(first.id, 0);
- } else if (event.key === 'End') {
- event.preventDefault();
- const last = available.at(-1);
- if (last) focusByOffset(last.id, 0);
- } else if ((event.key === 'Enter' || event.key === ' ') && activation === 'manual') {
- event.preventDefault();
- setActiveId(id);
- }
- };
-
- return (
-
-
- {items.map((item) => {
- const selected = item.id === activeId;
- return (
-
- );
- })}
-
- {items.map((item) => (
-
- {item.content}
-
- ))}
-
- );
-}
diff --git a/homepage/src/components/feedback/Alert.module.css b/homepage/src/components/feedback/Alert.module.css
deleted file mode 100644
index 4584410..0000000
--- a/homepage/src/components/feedback/Alert.module.css
+++ /dev/null
@@ -1,34 +0,0 @@
-.alert {
- display: grid;
- grid-template-columns: auto 1fr;
- gap: var(--space-3);
- padding: var(--space-4);
- border: 1px solid currentColor;
- border-radius: var(--radius-sm);
-}
-.inline {
- padding: var(--space-3);
-}
-.info {
- background: var(--status-info-surface);
- color: var(--status-info-text);
-}
-.success {
- background: var(--status-success-surface);
- color: var(--status-success-text);
-}
-.warning {
- background: var(--status-warning-surface);
- color: var(--status-warning-text);
-}
-.error {
- background: var(--status-error-surface);
- color: var(--status-error-text);
-}
-.title {
- display: block;
- margin-block-end: var(--space-1);
-}
-.content > :last-child {
- margin-block-end: 0;
-}
diff --git a/homepage/src/components/feedback/Alert.tsx b/homepage/src/components/feedback/Alert.tsx
deleted file mode 100644
index 0c27cb8..0000000
--- a/homepage/src/components/feedback/Alert.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import { Icon, type ApprovedIconName } from '@/components/foundation/Icon';
-import { classNames } from '@/lib/components/classNames';
-import type { ReactNode } from 'react';
-import styles from './Alert.module.css';
-
-export type AlertTone = 'info' | 'success' | 'warning' | 'error';
-
-const toneIcon: Record = {
- info: 'info',
- success: 'check',
- warning: 'warning',
- error: 'error',
-};
-
-interface AlertProps {
- tone?: AlertTone;
- title?: string;
- children: ReactNode;
- live?: 'off' | 'polite' | 'assertive';
- inline?: boolean;
- className?: string;
-}
-
-export function Alert({
- tone = 'info',
- title,
- children,
- live = 'off',
- inline = false,
- className,
-}: AlertProps) {
- return (
-
-
-
- {title ?
{title} : null}
-
{children}
-
-
- );
-}
diff --git a/homepage/src/components/feedback/Loading.module.css b/homepage/src/components/feedback/Loading.module.css
deleted file mode 100644
index 53c588c..0000000
--- a/homepage/src/components/feedback/Loading.module.css
+++ /dev/null
@@ -1,43 +0,0 @@
-.spinnerGroup {
- display: inline-flex;
- align-items: center;
- justify-content: center;
-}
-.spinner {
- animation: spin 0.9s linear infinite;
-}
-.skeleton {
- display: grid;
- gap: var(--space-3);
-}
-.skeleton span {
- block-size: 0.9rem;
- border-radius: var(--radius-pill);
- background: linear-gradient(
- 90deg,
- var(--skeleton-base),
- var(--skeleton-highlight),
- var(--skeleton-base)
- );
- background-size: 200% 100%;
- animation: shimmer 1.5s linear infinite;
-}
-@keyframes spin {
- to {
- transform: rotate(360deg);
- }
-}
-@keyframes shimmer {
- to {
- background-position: -200% 0;
- }
-}
-@media (prefers-reduced-motion: reduce) {
- .spinner {
- animation-duration: 1.8s;
- }
- .skeleton span {
- animation: none;
- background: var(--skeleton-base);
- }
-}
diff --git a/homepage/src/components/feedback/Loading.tsx b/homepage/src/components/feedback/Loading.tsx
deleted file mode 100644
index 3c96592..0000000
--- a/homepage/src/components/feedback/Loading.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import { Icon } from '@/components/foundation/Icon';
-import { classNames } from '@/lib/components/classNames';
-import type { HTMLAttributes } from 'react';
-import styles from './Loading.module.css';
-
-export function Spinner({ label, className }: { label: string; className?: string }) {
- return (
-
-
- {label}
-
- );
-}
-
-interface SkeletonProps extends HTMLAttributes {
- lines?: number;
- label?: string;
-}
-
-export function Skeleton({
- lines = 3,
- label = 'Loading content',
- className,
- ...props
-}: SkeletonProps) {
- return (
-
- {Array.from({ length: lines }, (_, index) => (
-
- ))}
-
- );
-}
diff --git a/homepage/src/components/feedback/Progress.module.css b/homepage/src/components/feedback/Progress.module.css
deleted file mode 100644
index 68266a7..0000000
--- a/homepage/src/components/feedback/Progress.module.css
+++ /dev/null
@@ -1,16 +0,0 @@
-.group {
- display: grid;
- gap: var(--space-2);
-}
-.labelRow {
- display: flex;
- justify-content: space-between;
- gap: var(--space-4);
- font-size: var(--type-label-size);
- font-weight: 700;
-}
-.progress {
- inline-size: 100%;
- block-size: 0.75rem;
- accent-color: var(--interactive-primary);
-}
diff --git a/homepage/src/components/feedback/Progress.tsx b/homepage/src/components/feedback/Progress.tsx
deleted file mode 100644
index a1f411e..0000000
--- a/homepage/src/components/feedback/Progress.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import { BidiText } from '@/components/foundation/BidiText';
-import { classNames } from '@/lib/components/classNames';
-import styles from './Progress.module.css';
-
-interface ProgressProps {
- value?: number;
- max?: number;
- label: string;
- showValue?: boolean;
- className?: string;
-}
-
-export function Progress({ value, max = 100, label, showValue = false, className }: ProgressProps) {
- const normalized = value === undefined ? undefined : Math.min(max, Math.max(0, value));
- return (
-
-
- {label}
- {showValue && normalized !== undefined ? (
- {`${Math.round((normalized / max) * 100)}%`}
- ) : null}
-
-
-
- );
-}
diff --git a/homepage/src/components/feedback/StateMessage.module.css b/homepage/src/components/feedback/StateMessage.module.css
deleted file mode 100644
index c0be04e..0000000
--- a/homepage/src/components/feedback/StateMessage.module.css
+++ /dev/null
@@ -1,17 +0,0 @@
-.state {
- display: grid;
- justify-items: start;
- max-inline-size: var(--container-reading);
- gap: var(--space-3);
- padding: var(--space-8);
- border: 1px dashed var(--border-strong);
- border-radius: var(--radius-md);
- background: var(--surface-muted);
-}
-.state h2,
-.state p {
- margin: 0;
-}
-.state p {
- color: var(--text-secondary);
-}
diff --git a/homepage/src/components/feedback/StateMessage.tsx b/homepage/src/components/feedback/StateMessage.tsx
deleted file mode 100644
index 7a3b18e..0000000
--- a/homepage/src/components/feedback/StateMessage.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { ActionLink } from '@/components/actions/ActionLink';
-import { Icon, type ApprovedIconName } from '@/components/foundation/Icon';
-import styles from './StateMessage.module.css';
-
-interface StateMessageProps {
- icon?: ApprovedIconName;
- title: string;
- body: string;
- action?: { label: string; href: string };
-}
-
-export function StateMessage({ icon = 'info', title, body, action }: StateMessageProps) {
- return (
-
-
- {title}
- {body}
- {action ? (
-
- {action.label}
-
- ) : null}
-
- );
-}
diff --git a/homepage/src/components/forms/ChoiceControls.module.css b/homepage/src/components/forms/ChoiceControls.module.css
deleted file mode 100644
index e0d7e08..0000000
--- a/homepage/src/components/forms/ChoiceControls.module.css
+++ /dev/null
@@ -1,77 +0,0 @@
-.choice {
- display: flex;
- min-block-size: 2.75rem;
- align-items: flex-start;
- gap: var(--space-3);
- cursor: pointer;
-}
-.choice > input:not(.switchInput) {
- inline-size: 1.25rem;
- block-size: 1.25rem;
- margin-block-start: 0.2rem;
- accent-color: var(--interactive-primary);
-}
-.content {
- display: grid;
- gap: var(--space-1);
-}
-.label {
- font-weight: 700;
-}
-.description {
- color: var(--text-secondary);
- font-size: var(--type-label-size);
-}
-.invalid .label {
- color: var(--status-error-text);
-}
-.switchInput {
- position: absolute;
- inline-size: 1px;
- block-size: 1px;
- opacity: 0;
-}
-.switchTrack {
- position: relative;
- flex: none;
- inline-size: 2.75rem;
- block-size: 1.5rem;
- margin-block-start: 0.1rem;
- border: 1px solid var(--border-strong);
- border-radius: var(--radius-pill);
- background: var(--surface-strong);
- transition: background var(--duration-fast) var(--easing-productive);
-}
-.switchTrack span {
- position: absolute;
- inset-block-start: 0.18rem;
- inset-inline-start: 0.2rem;
- inline-size: 1rem;
- block-size: 1rem;
- border-radius: 50%;
- background: var(--surface-elevated);
- box-shadow: var(--shadow-subtle);
- transition: transform var(--duration-fast) var(--easing-productive);
-}
-.switchInput:checked + .switchTrack {
- background: var(--interactive-primary);
-}
-.switchInput:checked + .switchTrack span {
- transform: translateX(1.2rem);
-}
-html[dir='rtl'] .switchInput:checked + .switchTrack span {
- transform: translateX(-1.2rem);
-}
-.switchInput:focus-visible + .switchTrack {
- outline: 3px solid var(--focus-ring);
- outline-offset: 3px;
-}
-.switchInput:disabled + .switchTrack {
- opacity: var(--opacity-disabled);
-}
-@media (prefers-reduced-motion: reduce) {
- .switchTrack,
- .switchTrack span {
- transition: none;
- }
-}
diff --git a/homepage/src/components/forms/ChoiceControls.tsx b/homepage/src/components/forms/ChoiceControls.tsx
deleted file mode 100644
index 3ad64ee..0000000
--- a/homepage/src/components/forms/ChoiceControls.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-'use client';
-
-import { classNames } from '@/lib/components/classNames';
-import type { InputHTMLAttributes, ReactNode } from 'react';
-import styles from './ChoiceControls.module.css';
-
-interface ChoiceProps extends Omit, 'type'> {
- label: ReactNode;
- description?: ReactNode;
- invalid?: boolean;
-}
-
-function Choice({
- label,
- description,
- invalid = false,
- className,
- type,
- ...props
-}: ChoiceProps & { type: 'checkbox' | 'radio' }) {
- return (
-
- );
-}
-
-export function Checkbox(props: ChoiceProps) {
- return ;
-}
-
-export function Radio(props: ChoiceProps) {
- return ;
-}
-
-interface SwitchProps extends Omit, 'type' | 'role'> {
- label: ReactNode;
- description?: ReactNode;
-}
-
-export function Switch({ label, description, className, ...props }: SwitchProps) {
- return (
-
- );
-}
diff --git a/homepage/src/components/forms/ErrorSummary.module.css b/homepage/src/components/forms/ErrorSummary.module.css
deleted file mode 100644
index cc762c8..0000000
--- a/homepage/src/components/forms/ErrorSummary.module.css
+++ /dev/null
@@ -1,24 +0,0 @@
-.summary {
- padding: var(--space-5);
- border: 2px solid var(--status-error-text);
- border-radius: var(--radius-sm);
- background: var(--status-error-surface);
- color: var(--status-error-text);
-}
-.heading {
- display: flex;
- align-items: center;
- gap: var(--space-2);
-}
-.heading h2 {
- margin: 0;
- font-size: var(--type-heading-3-size);
-}
-.summary ul {
- margin-block-end: 0;
- padding-inline-start: var(--space-6);
-}
-.summary a {
- color: currentColor;
- font-weight: 700;
-}
diff --git a/homepage/src/components/forms/ErrorSummary.tsx b/homepage/src/components/forms/ErrorSummary.tsx
deleted file mode 100644
index 42dd53d..0000000
--- a/homepage/src/components/forms/ErrorSummary.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import { Icon } from '@/components/foundation/Icon';
-import styles from './ErrorSummary.module.css';
-
-export interface FormErrorItem {
- fieldId: string;
- message: string;
-}
-
-export function ErrorSummary({ title, errors }: { title: string; errors: FormErrorItem[] }) {
- if (errors.length === 0) return null;
- return (
-
- );
-}
diff --git a/homepage/src/components/forms/FormField.module.css b/homepage/src/components/forms/FormField.module.css
deleted file mode 100644
index 6ac2f55..0000000
--- a/homepage/src/components/forms/FormField.module.css
+++ /dev/null
@@ -1,47 +0,0 @@
-.field {
- display: grid;
- gap: var(--space-2);
-}
-.label {
- display: flex;
- flex-wrap: wrap;
- align-items: baseline;
- gap: var(--space-2);
- font-weight: 700;
-}
-.required {
- color: var(--status-error-text);
-}
-.optional {
- color: var(--text-secondary);
- font-size: var(--type-caption-size);
- font-weight: 500;
-}
-.help,
-.error {
- margin: 0;
- font-size: var(--type-label-size);
-}
-.help {
- color: var(--text-secondary);
-}
-.error {
- color: var(--status-error-text);
- font-weight: 650;
-}
-.invalid .label {
- color: var(--status-error-text);
-}
-.fieldset {
- display: grid;
- gap: var(--space-4);
- min-inline-size: 0;
- margin: 0;
- padding: 0;
- border: 0;
-}
-.legend {
- margin-block-end: var(--space-3);
- padding: 0;
- font-weight: 750;
-}
diff --git a/homepage/src/components/forms/FormField.tsx b/homepage/src/components/forms/FormField.tsx
deleted file mode 100644
index 0010f83..0000000
--- a/homepage/src/components/forms/FormField.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-import { classNames } from '@/lib/components/classNames';
-import type { ReactNode } from 'react';
-import styles from './FormField.module.css';
-
-interface FormFieldProps {
- id: string;
- label: string;
- required?: boolean;
- optionalLabel?: string | undefined;
- supportingText?: string | undefined;
- error?: string | undefined;
- children: ReactNode;
- className?: string | undefined;
-}
-
-export function FormField({
- id,
- label,
- required = false,
- optionalLabel,
- supportingText,
- error,
- children,
- className,
-}: FormFieldProps) {
- return (
-
-
- {children}
- {supportingText ? (
-
- {supportingText}
-
- ) : null}
- {error ? (
-
- ! {error}
-
- ) : null}
-
- );
-}
-
-export function Fieldset({
- legend,
- children,
- className,
-}: {
- legend: string;
- children: ReactNode;
- className?: string | undefined;
-}) {
- return (
-
- );
-}
diff --git a/homepage/src/components/forms/TextInput.module.css b/homepage/src/components/forms/TextInput.module.css
deleted file mode 100644
index 0a7ad44..0000000
--- a/homepage/src/components/forms/TextInput.module.css
+++ /dev/null
@@ -1,43 +0,0 @@
-.control {
- inline-size: 100%;
- min-block-size: 2.875rem;
- padding-block: var(--space-3);
- padding-inline: var(--space-4);
- border: 1px solid var(--border-strong);
- border-radius: var(--radius-sm);
- background: var(--surface-primary);
- color: var(--text-primary);
-}
-.control::placeholder {
- color: var(--text-subdued);
- opacity: 1;
-}
-.control:hover:not(:disabled) {
- border-color: var(--interactive-primary);
-}
-.control[aria-invalid='true'] {
- border-color: var(--status-error-text);
- box-shadow: 0 0 0 1px var(--status-error-text);
-}
-.control:disabled {
- background: var(--control-disabled-surface);
- color: var(--control-disabled-text);
- cursor: not-allowed;
-}
-.textarea {
- min-block-size: 8rem;
- resize: vertical;
-}
-.textareaGroup {
- display: grid;
- gap: var(--space-1);
-}
-.count {
- margin: 0;
- color: var(--text-secondary);
- font-size: var(--type-caption-size);
- text-align: end;
-}
-.select {
- appearance: auto;
-}
diff --git a/homepage/src/components/forms/TextInput.tsx b/homepage/src/components/forms/TextInput.tsx
deleted file mode 100644
index 1b52f91..0000000
--- a/homepage/src/components/forms/TextInput.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-import { BidiText } from '@/components/foundation/BidiText';
-import { classNames } from '@/lib/components/classNames';
-import {
- forwardRef,
- type InputHTMLAttributes,
- type SelectHTMLAttributes,
- type TextareaHTMLAttributes,
-} from 'react';
-import styles from './TextInput.module.css';
-
-export type TextInputType = 'text' | 'email' | 'tel' | 'url' | 'search';
-
-interface TextInputProps extends Omit, 'type'> {
- type?: TextInputType;
- invalid?: boolean;
- describedBy?: string | undefined;
- bidiValue?: boolean;
-}
-
-export const TextInput = forwardRef(function TextInput(
- { type = 'text', invalid = false, describedBy, bidiValue = false, className, ...props },
- ref,
-) {
- const dir = bidiValue || type === 'email' || type === 'tel' || type === 'url' ? 'ltr' : undefined;
- return (
-
- );
-});
-
-interface TextAreaProps extends TextareaHTMLAttributes {
- invalid?: boolean;
- describedBy?: string | undefined;
- characterCount?: { current: number; max: number; label: string };
-}
-
-export function TextArea({
- invalid = false,
- describedBy,
- characterCount,
- className,
- ...props
-}: TextAreaProps) {
- const countId = characterCount && props.id ? `${props.id}-count` : undefined;
- const ids = [describedBy, countId].filter(Boolean).join(' ') || undefined;
- return (
-
-
- {characterCount ? (
-
- {`${characterCount.current}/${characterCount.max}`}{' '}
- {characterCount.label}
-
- ) : null}
-
- );
-}
-
-interface SelectProps extends SelectHTMLAttributes {
- invalid?: boolean;
- describedBy?: string | undefined;
-}
-
-export function Select({
- invalid = false,
- describedBy,
- className,
- children,
- ...props
-}: SelectProps) {
- return (
-
- );
-}
diff --git a/homepage/src/components/foundation/AccessibleIcon.tsx b/homepage/src/components/foundation/AccessibleIcon.tsx
deleted file mode 100644
index c0cc76a..0000000
--- a/homepage/src/components/foundation/AccessibleIcon.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import type { ReactElement } from 'react';
-
-interface AccessibleIconProps {
- icon: ReactElement;
- label?: string;
-}
-
-export function AccessibleIcon({ icon, label }: AccessibleIconProps) {
- if (label) {
- return (
-
- {icon}
-
- );
- }
- return {icon};
-}
diff --git a/homepage/src/components/foundation/BidiText.module.css b/homepage/src/components/foundation/BidiText.module.css
deleted file mode 100644
index e2a2701..0000000
--- a/homepage/src/components/foundation/BidiText.module.css
+++ /dev/null
@@ -1,3 +0,0 @@
-.noWrap {
- white-space: nowrap;
-}
diff --git a/homepage/src/components/foundation/BidiText.tsx b/homepage/src/components/foundation/BidiText.tsx
deleted file mode 100644
index f532c29..0000000
--- a/homepage/src/components/foundation/BidiText.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import { classNames } from '@/lib/components/classNames';
-import type { HTMLAttributes, ReactNode } from 'react';
-import styles from './BidiText.module.css';
-
-interface BidiTextProps extends Omit, 'dir'> {
- children: ReactNode;
- direction?: 'ltr' | 'rtl' | 'auto';
- noWrap?: boolean;
-}
-
-export function BidiText({
- children,
- direction = 'ltr',
- noWrap = true,
- className,
- ...props
-}: BidiTextProps) {
- return (
-
- {children}
-
- );
-}
diff --git a/homepage/src/components/foundation/Icon.module.css b/homepage/src/components/foundation/Icon.module.css
deleted file mode 100644
index d185c7e..0000000
--- a/homepage/src/components/foundation/Icon.module.css
+++ /dev/null
@@ -1,25 +0,0 @@
-.icon {
- flex: none;
- inline-size: 1.25rem;
- block-size: 1.25rem;
-}
-.sm {
- inline-size: 1rem;
- block-size: 1rem;
-}
-.md {
- inline-size: 1.25rem;
- block-size: 1.25rem;
-}
-.lg {
- inline-size: 1.5rem;
- block-size: 1.5rem;
-}
-html[dir='rtl'] .mirror {
- transform: scaleX(-1);
-}
-@media (prefers-reduced-motion: reduce) {
- .icon {
- animation: none !important;
- }
-}
diff --git a/homepage/src/components/foundation/Icon.tsx b/homepage/src/components/foundation/Icon.tsx
deleted file mode 100644
index 041439f..0000000
--- a/homepage/src/components/foundation/Icon.tsx
+++ /dev/null
@@ -1,139 +0,0 @@
-import { classNames } from '@/lib/components/classNames';
-import type { ReactNode, SVGProps } from 'react';
-import styles from './Icon.module.css';
-
-export const approvedIconNames = [
- 'arrow-back',
- 'arrow-forward',
- 'calendar',
- 'car',
- 'check',
- 'chevron-down',
- 'chevron-forward',
- 'close',
- 'error',
- 'external',
- 'globe',
- 'info',
- 'menu',
- 'minus',
- 'plus',
- 'search',
- 'settings',
- 'spinner',
- 'warning',
-] as const;
-
-export type ApprovedIconName = (typeof approvedIconNames)[number];
-export type IconSize = 'sm' | 'md' | 'lg';
-export type IconDirectionBehavior = 'fixed' | 'mirror';
-
-interface IconProps extends Omit, 'name'> {
- name: ApprovedIconName;
- size?: IconSize;
- decorative?: boolean;
- label?: string;
- directionBehavior?: IconDirectionBehavior;
-}
-
-const paths: Record = {
- 'arrow-back': ,
- 'arrow-forward': ,
- calendar: (
- <>
-
-
- >
- ),
- car: (
- <>
-
-
- >
- ),
- check: ,
- 'chevron-down': ,
- 'chevron-forward': ,
- close: ,
- error: (
- <>
-
-
- >
- ),
- external: (
- <>
-
-
- >
- ),
- globe: (
- <>
-
-
- >
- ),
- info: (
- <>
-
-
- >
- ),
- menu: ,
- minus: ,
- plus: ,
- search: (
- <>
-
-
- >
- ),
- settings: (
- <>
-
-
- >
- ),
- spinner: ,
- warning: (
- <>
-
-
- >
- ),
-};
-
-export function Icon({
- name,
- size = 'md',
- decorative = true,
- label,
- directionBehavior = 'fixed',
- className,
- ...props
-}: IconProps) {
- const accessibleProps = decorative
- ? ({ 'aria-hidden': true } as const)
- : ({ role: 'img', 'aria-label': label ?? name } as const);
- return (
-
- );
-}
diff --git a/homepage/src/components/foundation/LiveRegion.module.css b/homepage/src/components/foundation/LiveRegion.module.css
deleted file mode 100644
index 7ebf42b..0000000
--- a/homepage/src/components/foundation/LiveRegion.module.css
+++ /dev/null
@@ -1,11 +0,0 @@
-.region {
- position: absolute;
- inline-size: 1px;
- block-size: 1px;
- margin: -1px;
- padding: 0;
- overflow: hidden;
- border: 0;
- clip-path: inset(50%);
- white-space: nowrap;
-}
diff --git a/homepage/src/components/foundation/LiveRegion.tsx b/homepage/src/components/foundation/LiveRegion.tsx
deleted file mode 100644
index 8d0020b..0000000
--- a/homepage/src/components/foundation/LiveRegion.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import type { ReactNode } from 'react';
-import styles from './LiveRegion.module.css';
-
-interface LiveRegionProps {
- children: ReactNode;
- politeness?: 'polite' | 'assertive';
-}
-
-export function LiveRegion({ children, politeness = 'polite' }: LiveRegionProps) {
- return (
-
- {children}
-
- );
-}
diff --git a/homepage/src/components/foundation/StatusBadge.module.css b/homepage/src/components/foundation/StatusBadge.module.css
deleted file mode 100644
index 4c1100d..0000000
--- a/homepage/src/components/foundation/StatusBadge.module.css
+++ /dev/null
@@ -1,34 +0,0 @@
-.badge {
- display: inline-flex;
- min-block-size: 1.75rem;
- align-items: center;
- gap: var(--space-1);
- inline-size: fit-content;
- padding-block: var(--space-1);
- padding-inline: var(--space-3);
- border: 1px solid currentColor;
- border-radius: var(--radius-pill);
- font-size: var(--type-caption-size);
- font-weight: 800;
- line-height: 1.2;
-}
-.neutral {
- background: var(--surface-muted);
- color: var(--text-secondary);
-}
-.info {
- background: var(--status-info-surface);
- color: var(--status-info-text);
-}
-.success {
- background: var(--status-success-surface);
- color: var(--status-success-text);
-}
-.warning {
- background: var(--status-warning-surface);
- color: var(--status-warning-text);
-}
-.error {
- background: var(--status-error-surface);
- color: var(--status-error-text);
-}
diff --git a/homepage/src/components/foundation/StatusBadge.tsx b/homepage/src/components/foundation/StatusBadge.tsx
deleted file mode 100644
index 2792fcb..0000000
--- a/homepage/src/components/foundation/StatusBadge.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import { Icon, type ApprovedIconName } from './Icon';
-import styles from './StatusBadge.module.css';
-
-export type StatusTone = 'neutral' | 'info' | 'success' | 'warning' | 'error';
-
-const icons: Partial> = {
- info: 'info',
- success: 'check',
- warning: 'warning',
- error: 'error',
-};
-
-export function StatusBadge({
- children,
- tone = 'neutral',
- showIcon = false,
-}: {
- children: string;
- tone?: StatusTone;
- showIcon?: boolean;
-}) {
- const icon = icons[tone];
- return (
-
- {showIcon && icon ? : null}
- {children}
-
- );
-}
diff --git a/homepage/src/components/foundation/VisuallyHidden.tsx b/homepage/src/components/foundation/VisuallyHidden.tsx
deleted file mode 100644
index 3958dd3..0000000
--- a/homepage/src/components/foundation/VisuallyHidden.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import type { HTMLAttributes, ReactNode } from 'react';
-
-interface VisuallyHiddenProps extends HTMLAttributes {
- children: ReactNode;
-}
-
-export function VisuallyHidden({ children, className, ...props }: VisuallyHiddenProps) {
- const classes = ['visually-hidden', className].filter(Boolean).join(' ');
- return (
-
- {children}
-
- );
-}
diff --git a/homepage/src/components/foundation/client-hooks.ts b/homepage/src/components/foundation/client-hooks.ts
deleted file mode 100644
index 6297d33..0000000
--- a/homepage/src/components/foundation/client-hooks.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-'use client';
-
-import { useEffect, useId, useState, useSyncExternalStore, type RefObject } from 'react';
-
-export function useAccessibleId(prefix = 'rdg'): string {
- return `${prefix}-${useId().replaceAll(':', '')}`;
-}
-
-const subscribeHydration = () => () => undefined;
-
-export function useHydrated(): boolean {
- return useSyncExternalStore(
- subscribeHydration,
- () => true,
- () => false,
- );
-}
-
-export function useMediaQuery(query: string): boolean {
- const [matches, setMatches] = useState(false);
- useEffect(() => {
- const media = window.matchMedia(query);
- const update = () => setMatches(media.matches);
- update();
- media.addEventListener('change', update);
- return () => media.removeEventListener('change', update);
- }, [query]);
- return matches;
-}
-
-export function useReducedMotion(): boolean {
- return useMediaQuery('(prefers-reduced-motion: reduce)');
-}
-
-export function useEscapeKey(onEscape: () => void, enabled = true): void {
- useEffect(() => {
- if (!enabled) return;
- const handleKeyDown = (event: KeyboardEvent) => {
- if (event.key === 'Escape') onEscape();
- };
- document.addEventListener('keydown', handleKeyDown);
- return () => document.removeEventListener('keydown', handleKeyDown);
- }, [enabled, onEscape]);
-}
-
-export function useOutsideClick(
- ref: RefObject,
- onOutside: () => void,
- enabled = true,
-): void {
- useEffect(() => {
- if (!enabled) return;
- const handlePointerDown = (event: PointerEvent) => {
- if (ref.current && !ref.current.contains(event.target as Node)) onOutside();
- };
- document.addEventListener('pointerdown', handlePointerDown);
- return () => document.removeEventListener('pointerdown', handlePointerDown);
- }, [enabled, onOutside, ref]);
-}
-
-export function useScrollLock(locked: boolean): void {
- useEffect(() => {
- if (!locked) return;
- const previousOverflow = document.body.style.overflow;
- const previousPadding = document.body.style.paddingInlineEnd;
- const scrollbar = window.innerWidth - document.documentElement.clientWidth;
- document.body.style.overflow = 'hidden';
- if (scrollbar > 0) document.body.style.paddingInlineEnd = `${scrollbar}px`;
- return () => {
- document.body.style.overflow = previousOverflow;
- document.body.style.paddingInlineEnd = previousPadding;
- };
- }, [locked]);
-}
diff --git a/homepage/src/components/integrations/DemoDialogHost.module.css b/homepage/src/components/integrations/DemoDialogHost.module.css
deleted file mode 100644
index 568069d..0000000
--- a/homepage/src/components/integrations/DemoDialogHost.module.css
+++ /dev/null
@@ -1,76 +0,0 @@
-.form {
- display: grid;
- gap: var(--space-5);
-}
-
-.notice {
- display: grid;
- gap: var(--space-2);
- padding: var(--space-4);
- border: 1px solid var(--border-standard);
- border-radius: var(--radius-sm);
- background: var(--surface-muted);
-}
-
-.notice p,
-.statusText {
- margin: 0;
- color: var(--text-secondary);
-}
-
-.grid {
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: var(--space-4);
-}
-
-.spanAll {
- grid-column: 1 / -1;
-}
-
-.actions {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- gap: var(--space-3);
-}
-
-.success {
- display: grid;
- gap: var(--space-4);
- text-align: start;
-}
-
-.success h3,
-.success p {
- margin: 0;
-}
-
-.honeypot {
- position: absolute;
- inline-size: 1px;
- block-size: 1px;
- overflow: hidden;
- clip-path: inset(50%);
- white-space: nowrap;
-}
-
-@media (max-width: 639px) {
- .grid {
- grid-template-columns: minmax(0, 1fr);
- }
-
- .spanAll {
- grid-column: auto;
- }
-
- .actions > * {
- inline-size: 100%;
- }
-}
-
-@media (forced-colors: active) {
- .notice {
- border: 1px solid CanvasText;
- }
-}
diff --git a/homepage/src/components/integrations/DemoDialogHost.tsx b/homepage/src/components/integrations/DemoDialogHost.tsx
deleted file mode 100644
index 0d77336..0000000
--- a/homepage/src/components/integrations/DemoDialogHost.tsx
+++ /dev/null
@@ -1,449 +0,0 @@
-'use client';
-
-import { Button } from '@/components/actions/Button';
-import { ErrorSummary, type FormErrorItem } from '@/components/forms/ErrorSummary';
-import { FormField } from '@/components/forms/FormField';
-import { Select, TextArea, TextInput } from '@/components/forms/TextInput';
-import { LiveRegion } from '@/components/foundation/LiveRegion';
-import { StatusBadge } from '@/components/foundation/StatusBadge';
-import { Dialog } from '@/components/overlays/Dialog';
-import type { HomepageMessages } from '@/lib/localization/messages';
-import { trackAnalytics } from '@/lib/analytics/client';
-import type { AnalyticsContext } from '@/lib/analytics/events';
-import {
- demoLeadSchema,
- fleetSizeValues,
- type DemoSource,
- type DemoSubmissionResult,
-} from '@/lib/demo/schema';
-import type { Locale } from '@/lib/localization/config';
-import { getDirection } from '@/lib/localization/config';
-import { useEffect, useMemo, useRef, useState } from 'react';
-import { demoOpenEventName, type DemoOpenEventDetail } from './DemoTrigger';
-import styles from './DemoDialogHost.module.css';
-
-interface DemoDialogHostProps {
- locale: Locale;
- messages: HomepageMessages['form'];
- enabled: boolean;
-}
-
-type FormStatus = 'idle' | 'submitting' | 'success' | 'error';
-type FieldErrors = Record;
-
-function messageForCode(code: string, messages: DemoDialogHostProps['messages']): string {
- switch (code) {
- case 'required':
- return messages.requiredError;
- case 'invalid_email':
- return messages.emailError;
- case 'invalid_fleet_size':
- case 'invalid_source':
- case 'invalid_idempotency_key':
- return messages.invalidOptionError;
- case 'too_long':
- return messages.tooLongError;
- default:
- return messages.genericError;
- }
-}
-
-function formMessage(
- result: Extract,
- messages: DemoDialogHostProps['messages'],
-) {
- switch (result.category) {
- case 'configuration':
- case 'consent':
- return messages.configurationError;
- case 'timeout':
- return messages.timeoutError;
- case 'duplicate':
- return messages.duplicateError;
- case 'rate-limit':
- return messages.rateLimitError;
- case 'validation':
- return messages.errorSummary;
- case 'integration':
- case 'abuse':
- return messages.genericError;
- }
-}
-
-function analyticsContext(
- locale: Locale,
- source: DemoSource,
- extra: Partial> = {},
-): AnalyticsContext {
- const theme = document.documentElement.dataset.theme === 'dark' ? 'dark' : 'light';
- return {
- routeId: 'home',
- locale,
- direction: getDirection(locale),
- resolvedTheme: theme,
- source,
- ...extra,
- };
-}
-
-export function DemoDialogHost({ locale, messages, enabled }: DemoDialogHostProps) {
- const [open, setOpen] = useState(false);
- const [source, setSource] = useState('hero');
- const [status, setStatus] = useState('idle');
- const [fieldErrors, setFieldErrors] = useState({});
- const [formError, setFormError] = useState('');
- const [messageLength, setMessageLength] = useState(0);
- const formRef = useRef(null);
- const firstFieldRef = useRef(null);
- const returnFocusRef = useRef(null);
- const idempotencyKeyRef = useRef('');
- const startedAtRef = useRef(0);
- const closeTrackedRef = useRef(false);
-
- const summaryErrors = useMemo(
- () =>
- Object.entries(fieldErrors).map(([fieldId, code]) => ({
- fieldId,
- message: messageForCode(code, messages),
- })),
- [fieldErrors, messages],
- );
-
- useEffect(() => {
- const handleOpen = (event: Event) => {
- const detail = (event as CustomEvent).detail;
- if (!detail || !enabled) return;
- returnFocusRef.current = detail.trigger;
- setSource(detail.source);
- closeTrackedRef.current = false;
- setOpen(true);
- setStatus('idle');
- setFieldErrors({});
- setFormError('');
- idempotencyKeyRef.current = crypto.randomUUID();
- startedAtRef.current = Date.now();
- trackAnalytics('demo_open', analyticsContext(locale, detail.source));
- };
- window.addEventListener(demoOpenEventName, handleOpen);
- return () => window.removeEventListener(demoOpenEventName, handleOpen);
- }, [enabled, locale]);
-
- const reset = () => {
- formRef.current?.reset();
- setMessageLength(0);
- setStatus('idle');
- setFieldErrors({});
- setFormError('');
- idempotencyKeyRef.current = crypto.randomUUID();
- startedAtRef.current = Date.now();
- window.requestAnimationFrame(() => firstFieldRef.current?.focus());
- };
-
- const close = () => {
- if (status === 'submitting' || closeTrackedRef.current) return;
- closeTrackedRef.current = true;
- setOpen(false);
- trackAnalytics('demo_close', analyticsContext(locale, source, { reason: 'user' }));
- };
-
- return (
-
- );
-}
diff --git a/homepage/src/components/integrations/DemoTrigger.tsx b/homepage/src/components/integrations/DemoTrigger.tsx
deleted file mode 100644
index ba3ed80..0000000
--- a/homepage/src/components/integrations/DemoTrigger.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-'use client';
-
-import { Button } from '@/components/actions/Button';
-import { ActionLink } from '@/components/actions/ActionLink';
-import type { DemoSource } from '@/lib/demo/schema';
-
-export const demoOpenEventName = 'rdg:demo-open';
-
-export interface DemoOpenEventDetail {
- source: DemoSource;
- trigger: HTMLElement;
-}
-
-interface DemoTriggerProps {
- label: string;
- source: DemoSource;
- disabledReason?: string | undefined;
- size?: 'small' | 'medium' | 'large';
- fullWidth?: boolean;
- className?: string | undefined;
-}
-
-export function DemoTrigger({
- label,
- source,
- disabledReason,
- size = 'medium',
- fullWidth = false,
- className,
-}: DemoTriggerProps) {
- if (disabledReason) {
- return (
-
- {label}
-
- );
- }
-
- return (
-
- );
-}
diff --git a/homepage/src/components/layout/LayoutPrimitives.module.css b/homepage/src/components/layout/LayoutPrimitives.module.css
deleted file mode 100644
index 71b34f2..0000000
--- a/homepage/src/components/layout/LayoutPrimitives.module.css
+++ /dev/null
@@ -1,81 +0,0 @@
-.container {
- inline-size: min(100%, var(--container-wide));
- margin-inline: auto;
- padding-inline: var(--layout-gutter);
-}
-
-.content {
- max-inline-size: var(--container-content);
-}
-
-.reading {
- max-inline-size: var(--container-reading);
-}
-
-.full {
- max-inline-size: none;
-}
-
-.section {
- padding-block: var(--section-space);
-}
-
-.sectionMuted {
- background: var(--surface-muted);
-}
-
-.sectionStrong {
- background: var(--surface-strong);
-}
-
-.stack {
- display: flex;
- flex-direction: column;
-}
-
-.stackSmall {
- gap: var(--space-3);
-}
-
-.stackMedium {
- gap: var(--space-6);
-}
-
-.stackLarge {
- gap: var(--space-12);
-}
-
-.cluster {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
-}
-
-.clusterSmall {
- gap: var(--space-2);
-}
-
-.clusterMedium {
- gap: var(--space-4);
-}
-
-.clusterLarge {
- gap: var(--space-8);
-}
-
-.grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
- gap: var(--space-6);
-}
-
-.bleed {
- inline-size: 100vw;
- margin-inline: calc(50% - 50vw);
-}
-
-.sticky {
- position: sticky;
- z-index: var(--layer-sticky);
- inset-block-start: 0;
-}
diff --git a/homepage/src/components/layout/LayoutPrimitives.tsx b/homepage/src/components/layout/LayoutPrimitives.tsx
deleted file mode 100644
index 6332295..0000000
--- a/homepage/src/components/layout/LayoutPrimitives.tsx
+++ /dev/null
@@ -1,92 +0,0 @@
-import type { HTMLAttributes, ReactNode } from 'react';
-import styles from './LayoutPrimitives.module.css';
-
-type ContainerWidth = 'wide' | 'content' | 'reading' | 'full';
-type Gap = 'small' | 'medium' | 'large';
-type SectionTone = 'default' | 'muted' | 'strong';
-
-function classes(...values: Array): string {
- return values.filter(Boolean).join(' ');
-}
-
-export function Container({
- children,
- width = 'wide',
- className,
- ...props
-}: HTMLAttributes & { children: ReactNode; width?: ContainerWidth }) {
- return (
-
- {children}
-
- );
-}
-
-export function Section({
- children,
- tone = 'default',
- className,
- ...props
-}: HTMLAttributes & { children: ReactNode; tone?: SectionTone }) {
- return (
-
- );
-}
-
-export function Stack({
- children,
- gap = 'medium',
- className,
- ...props
-}: HTMLAttributes & { children: ReactNode; gap?: Gap }) {
- const gapClass = {
- small: styles.stackSmall,
- medium: styles.stackMedium,
- large: styles.stackLarge,
- }[gap];
- return (
-
- {children}
-
- );
-}
-
-export function Cluster({
- children,
- gap = 'medium',
- className,
- ...props
-}: HTMLAttributes & { children: ReactNode; gap?: Gap }) {
- const gapClass = {
- small: styles.clusterSmall,
- medium: styles.clusterMedium,
- large: styles.clusterLarge,
- }[gap];
- return (
-
- {children}
-
- );
-}
-
-export function ResponsiveGrid({
- children,
- className,
- ...props
-}: HTMLAttributes & { children: ReactNode }) {
- return (
-
- {children}
-
- );
-}
diff --git a/homepage/src/components/marketing/CTA.module.css b/homepage/src/components/marketing/CTA.module.css
deleted file mode 100644
index d42d4c6..0000000
--- a/homepage/src/components/marketing/CTA.module.css
+++ /dev/null
@@ -1,42 +0,0 @@
-.cta {
- display: grid;
- grid-template-columns: minmax(0, 1fr) auto;
- align-items: center;
- gap: var(--space-8);
-}
-.copy {
- display: grid;
- gap: var(--space-3);
-}
-.eyebrow {
- margin: 0;
- color: var(--interactive-primary);
- font-size: var(--type-label-size);
- font-weight: 800;
-}
-.actions {
- display: flex;
- flex-wrap: wrap;
- justify-content: flex-end;
- gap: var(--space-3);
-}
-.inline {
- border-radius: var(--radius-sm);
-}
-.card {
- grid-template-columns: 1fr;
-}
-.card .actions {
- justify-content: flex-start;
-}
-@media (max-width: 767px) {
- .cta {
- grid-template-columns: 1fr;
- }
- .actions {
- justify-content: flex-start;
- }
- .actions > * {
- inline-size: 100%;
- }
-}
diff --git a/homepage/src/components/marketing/CTA.tsx b/homepage/src/components/marketing/CTA.tsx
deleted file mode 100644
index 5f883a3..0000000
--- a/homepage/src/components/marketing/CTA.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-import { ActionLink } from '@/components/actions/ActionLink';
-import { Card } from '@/components/surfaces/Card';
-import { Heading, Text, type HeadingLevel } from '@/components/typography/Typography';
-import type { ReactNode } from 'react';
-import styles from './CTA.module.css';
-
-export interface CTAAction {
- label: string;
- href?: string;
- disabledReason?: string;
-}
-
-interface CTAProps {
- eyebrow?: string;
- title: string;
- body?: string;
- primary: CTAAction;
- secondary?: CTAAction;
- headingLevel?: HeadingLevel;
- layout?: 'inline' | 'section' | 'card';
- primaryNode?: ReactNode;
- secondaryNode?: ReactNode;
-}
-
-export function CTA({
- eyebrow,
- title,
- body,
- primary,
- secondary,
- headingLevel = 2,
- layout = 'section',
- primaryNode,
- secondaryNode,
-}: CTAProps) {
- return (
-
-
- {eyebrow ?
{eyebrow}
: null}
-
- {title}
-
- {body ?
{body} : null}
-
-
- {primaryNode ?? (
-
- {primary.label}
-
- )}
- {secondaryNode ??
- (secondary ? (
-
- {secondary.label}
-
- ) : null)}
-
-
- );
-}
diff --git a/homepage/src/components/marketing/Comparison.module.css b/homepage/src/components/marketing/Comparison.module.css
deleted file mode 100644
index ca40c0a..0000000
--- a/homepage/src/components/marketing/Comparison.module.css
+++ /dev/null
@@ -1,35 +0,0 @@
-.wrapper {
- display: grid;
- gap: var(--space-6);
-}
-.grid {
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: var(--space-6);
-}
-.column {
- display: grid;
- align-content: start;
- gap: var(--space-4);
-}
-.column ul {
- display: grid;
- gap: var(--space-3);
- margin: 0;
- padding: 0;
- list-style: none;
-}
-.column li {
- display: grid;
- grid-template-columns: auto 1fr;
- gap: var(--space-2);
- align-items: start;
-}
-.column li svg {
- margin-block-start: 0.25rem;
-}
-@media (max-width: 767px) {
- .grid {
- grid-template-columns: 1fr;
- }
-}
diff --git a/homepage/src/components/marketing/Comparison.tsx b/homepage/src/components/marketing/Comparison.tsx
deleted file mode 100644
index 935a38b..0000000
--- a/homepage/src/components/marketing/Comparison.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import { Icon } from '@/components/foundation/Icon';
-import { Card } from '@/components/surfaces/Card';
-import { Heading, Text, type HeadingLevel } from '@/components/typography/Typography';
-import styles from './Comparison.module.css';
-
-export interface ComparisonColumn {
- title: string;
- items: string[];
- tone: 'problem' | 'solution';
-}
-
-interface ComparisonProps {
- title?: string;
- body?: string;
- columns: [ComparisonColumn, ComparisonColumn];
- headingLevel?: HeadingLevel;
-}
-
-export function Comparison({ title, body, columns, headingLevel = 2 }: ComparisonProps) {
- return (
-
- {title ? (
-
- {title}
-
- ) : null}
- {body ?
{body} : null}
-
- {columns.map((column) => (
-
-
- {column.title}
-
-
- {column.items.map((item) => (
- -
-
- {item}
-
- ))}
-
-
- ))}
-
-
- );
-}
diff --git a/homepage/src/components/marketing/Evidence.module.css b/homepage/src/components/marketing/Evidence.module.css
deleted file mode 100644
index 3d600b9..0000000
--- a/homepage/src/components/marketing/Evidence.module.css
+++ /dev/null
@@ -1,25 +0,0 @@
-.metric {
- display: grid;
- align-content: start;
- gap: var(--space-3);
-}
-.value {
- display: block;
- font-size: var(--type-heading-1-size);
- font-weight: 850;
- line-height: 1;
-}
-.source {
- color: var(--text-secondary);
-}
-.shell {
- display: grid;
- gap: var(--space-4);
-}
-.shellHeader {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- justify-content: space-between;
- gap: var(--space-3);
-}
diff --git a/homepage/src/components/marketing/Evidence.tsx b/homepage/src/components/marketing/Evidence.tsx
deleted file mode 100644
index b74dd11..0000000
--- a/homepage/src/components/marketing/Evidence.tsx
+++ /dev/null
@@ -1,81 +0,0 @@
-import { BidiText } from '@/components/foundation/BidiText';
-import { StatusBadge } from '@/components/foundation/StatusBadge';
-import { Card } from '@/components/surfaces/Card';
-import { Heading, Text, type HeadingLevel } from '@/components/typography/Typography';
-import type { ReactNode } from 'react';
-import styles from './Evidence.module.css';
-
-export type EvidenceStatus = 'approved' | 'pending-review' | 'research-only' | 'prohibited';
-
-export interface MetricContent {
- value: string;
- label: string;
- qualification?: string;
- sourceReference?: string;
- status: EvidenceStatus;
-}
-
-const statusTone = {
- approved: 'success',
- 'pending-review': 'warning',
- 'research-only': 'info',
- prohibited: 'error',
-} as const;
-
-export function canRenderProductionEvidence(status: EvidenceStatus): boolean {
- return status === 'approved';
-}
-
-interface MetricProps {
- content: MetricContent;
- statusLabel: string;
- production?: boolean;
- headingLevel?: HeadingLevel;
-}
-
-export function Metric({
- content,
- statusLabel,
- production = false,
- headingLevel = 3,
-}: MetricProps) {
- if (production && !canRenderProductionEvidence(content.status)) return null;
- return (
-
- {statusLabel}
- {content.value}
-
- {content.label}
-
- {content.qualification ? {content.qualification} : null}
- {content.sourceReference ? (
- {content.sourceReference}
- ) : null}
-
- );
-}
-
-export function EvidenceShell({
- title,
- status,
- statusLabel,
- children,
-}: {
- title: string;
- status: EvidenceStatus;
- statusLabel: string;
- children?: ReactNode;
-}) {
- if (status === 'prohibited') return null;
- return (
-
-
-
- {title}
-
- {statusLabel}
-
- {children}
-
- );
-}
diff --git a/homepage/src/components/marketing/FeatureCard.module.css b/homepage/src/components/marketing/FeatureCard.module.css
deleted file mode 100644
index 9c1c6bd..0000000
--- a/homepage/src/components/marketing/FeatureCard.module.css
+++ /dev/null
@@ -1,18 +0,0 @@
-.card {
- display: grid;
- align-content: start;
- gap: var(--space-4);
- block-size: 100%;
-}
-.icon {
- display: grid;
- place-items: center;
- inline-size: 3rem;
- block-size: 3rem;
- border-radius: var(--radius-sm);
- background: var(--status-info-surface);
- color: var(--status-info-text);
-}
-.card > :last-child {
- margin-block-start: auto;
-}
diff --git a/homepage/src/components/marketing/FeatureCard.tsx b/homepage/src/components/marketing/FeatureCard.tsx
deleted file mode 100644
index b22dbcd..0000000
--- a/homepage/src/components/marketing/FeatureCard.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import { ActionLink } from '@/components/actions/ActionLink';
-import { Icon, type ApprovedIconName } from '@/components/foundation/Icon';
-import { Card } from '@/components/surfaces/Card';
-import { Heading, Text, type HeadingLevel } from '@/components/typography/Typography';
-import styles from './FeatureCard.module.css';
-
-interface FeatureCardProps {
- title: string;
- description: string;
- icon?: ApprovedIconName;
- headingLevel?: HeadingLevel;
- emphasis?: 'default' | 'primary' | 'muted';
- action?: { label: string; href: string; disabledReason?: string };
-}
-
-export function FeatureCard({
- title,
- description,
- icon,
- headingLevel = 3,
- emphasis = 'default',
- action,
-}: FeatureCardProps) {
- return (
-
- {icon ? (
-
-
-
- ) : null}
-
- {title}
-
- {description}
- {action ? (
-
- {action.label}
-
- ) : null}
-
- );
-}
diff --git a/homepage/src/components/marketing/ProductPreview.module.css b/homepage/src/components/marketing/ProductPreview.module.css
deleted file mode 100644
index 2dafc15..0000000
--- a/homepage/src/components/marketing/ProductPreview.module.css
+++ /dev/null
@@ -1,69 +0,0 @@
-.figure {
- display: grid;
- gap: var(--space-3);
- margin: 0;
-}
-.preview {
- overflow: hidden;
-}
-.header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: var(--space-4);
- padding: var(--space-5);
- border-block-end: 1px solid var(--border-standard);
-}
-.header > div {
- display: grid;
- gap: var(--space-1);
-}
-.dots {
- display: flex;
- gap: var(--space-1);
-}
-.dots i {
- inline-size: 0.5rem;
- block-size: 0.5rem;
- border-radius: 50%;
- background: var(--border-strong);
-}
-.table {
- display: grid;
-}
-.row {
- display: grid;
- grid-template-columns: minmax(0, 1fr) auto;
- align-items: center;
- gap: var(--space-4);
- padding: var(--space-4) var(--space-5);
- border-block-end: 1px solid var(--border-standard);
-}
-.row:last-child {
- border-block-end: 0;
-}
-.row > div:first-child {
- display: grid;
- gap: var(--space-1);
- min-inline-size: 0;
-}
-.secondary {
- color: var(--text-secondary);
- font-size: var(--type-caption-size);
- overflow-wrap: anywhere;
-}
-.empty {
- margin: 0;
- padding: var(--space-8);
- color: var(--text-secondary);
- text-align: center;
-}
-.figure figcaption {
- color: var(--text-secondary);
- font-size: var(--type-caption-size);
-}
-@media (max-width: 479px) {
- .row {
- grid-template-columns: 1fr;
- }
-}
diff --git a/homepage/src/components/marketing/ProductPreview.tsx b/homepage/src/components/marketing/ProductPreview.tsx
deleted file mode 100644
index 2a4cfd9..0000000
--- a/homepage/src/components/marketing/ProductPreview.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import { BidiText } from '@/components/foundation/BidiText';
-import { StatusBadge, type StatusTone } from '@/components/foundation/StatusBadge';
-import { Card } from '@/components/surfaces/Card';
-import { Heading, Text, type HeadingLevel } from '@/components/typography/Typography';
-import styles from './ProductPreview.module.css';
-
-export interface PreviewRow {
- id: string;
- primary: string;
- secondary: string;
- status: string;
- statusTone?: StatusTone;
-}
-
-interface ProductPreviewProps {
- title: string;
- caption: string;
- rows: PreviewRow[];
- illustrativeLabel: string;
- emptyLabel?: string;
- headingLevel?: HeadingLevel;
-}
-
-export function ProductPreview({
- title,
- caption,
- rows,
- illustrativeLabel,
- emptyLabel = 'No preview data',
- headingLevel = 3,
-}: ProductPreviewProps) {
- return (
-
-
-
- {rows.length > 0 ? (
-
- {rows.map((row) => (
-
-
- {row.primary}
- {row.secondary}
-
-
-
- {row.status}
-
-
-
- ))}
-
- ) : (
- {emptyLabel}
- )}
-
- {caption}
-
- );
-}
diff --git a/homepage/src/components/marketing/SectionHeader.module.css b/homepage/src/components/marketing/SectionHeader.module.css
deleted file mode 100644
index 9c95cef..0000000
--- a/homepage/src/components/marketing/SectionHeader.module.css
+++ /dev/null
@@ -1,13 +0,0 @@
-.header {
- display: grid;
- gap: var(--space-4);
- max-inline-size: var(--container-reading);
-}
-.center {
- margin-inline: auto;
- justify-items: center;
- text-align: center;
-}
-.center p {
- margin-inline: auto;
-}
diff --git a/homepage/src/components/marketing/SectionHeader.tsx b/homepage/src/components/marketing/SectionHeader.tsx
deleted file mode 100644
index 38e6a0e..0000000
--- a/homepage/src/components/marketing/SectionHeader.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import { Eyebrow, Heading, Text, type HeadingLevel } from '@/components/typography/Typography';
-import { classNames } from '@/lib/components/classNames';
-import styles from './SectionHeader.module.css';
-
-interface SectionHeaderProps {
- eyebrow?: string;
- title: string;
- body?: string;
- headingLevel?: HeadingLevel;
- alignment?: 'start' | 'center';
- className?: string;
-}
-
-export function SectionHeader({
- eyebrow,
- title,
- body,
- headingLevel = 2,
- alignment = 'start',
- className,
-}: SectionHeaderProps) {
- return (
-
- {eyebrow ? {eyebrow} : null}
-
- {title}
-
- {body ? {body} : null}
-
- );
-}
diff --git a/homepage/src/components/marketing/SectionPatterns.module.css b/homepage/src/components/marketing/SectionPatterns.module.css
deleted file mode 100644
index 66d56ad..0000000
--- a/homepage/src/components/marketing/SectionPatterns.module.css
+++ /dev/null
@@ -1,44 +0,0 @@
-.featureGrid {
- grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
-}
-.split {
- display: grid;
- grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
- align-items: center;
- gap: clamp(var(--space-8), 7vw, var(--space-16));
-}
-.copy {
- min-inline-size: 0;
-}
-.media {
- min-inline-size: 0;
-}
-.reverse .copy {
- order: 2;
-}
-.reverse .media {
- order: 1;
-}
-.metricStrip {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(min(100%, 12rem), 1fr));
- gap: var(--space-4);
-}
-.logoStrip {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- justify-content: center;
- gap: var(--space-6);
- padding: var(--space-6);
- border-block: 1px solid var(--border-standard);
-}
-@media (max-width: 767px) {
- .split {
- grid-template-columns: 1fr;
- }
- .reverse .copy,
- .reverse .media {
- order: initial;
- }
-}
diff --git a/homepage/src/components/marketing/SectionPatterns.tsx b/homepage/src/components/marketing/SectionPatterns.tsx
deleted file mode 100644
index 91d697d..0000000
--- a/homepage/src/components/marketing/SectionPatterns.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import { ResponsiveGrid } from '@/components/layout/LayoutPrimitives';
-import type { ReactNode } from 'react';
-import styles from './SectionPatterns.module.css';
-
-export function FeatureGrid({ children }: { children: ReactNode }) {
- return {children};
-}
-
-export function SplitContent({
- copy,
- media,
- reverse = false,
-}: {
- copy: ReactNode;
- media: ReactNode;
- reverse?: boolean;
-}) {
- return (
-
- );
-}
-
-export function MetricStrip({ children }: { children: ReactNode }) {
- return {children}
;
-}
-
-export function LogoStripShell({ label, children }: { label: string; children: ReactNode }) {
- return (
-
- {children}
-
- );
-}
diff --git a/homepage/src/components/marketing/Workflow.module.css b/homepage/src/components/marketing/Workflow.module.css
deleted file mode 100644
index 0217ccd..0000000
--- a/homepage/src/components/marketing/Workflow.module.css
+++ /dev/null
@@ -1,48 +0,0 @@
-.workflow {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(min(100%, 13rem), 1fr));
- gap: var(--space-6);
- margin: 0;
- padding: 0;
- list-style: none;
- counter-reset: workflow;
-}
-.step {
- position: relative;
- display: grid;
- grid-template-columns: auto 1fr;
- align-content: start;
- gap: var(--space-4);
- min-inline-size: 0;
-}
-.marker {
- position: relative;
- z-index: 1;
- display: grid;
- place-items: center;
- inline-size: 3rem;
- block-size: 3rem;
- border: 2px solid var(--interactive-primary);
- border-radius: 50%;
- background: var(--surface-elevated);
- color: var(--interactive-primary);
- font-weight: 800;
-}
-.content {
- display: grid;
- gap: var(--space-2);
-}
-.connector {
- position: absolute;
- inset-block-start: 0.8rem;
- inset-inline-end: calc(var(--space-6) * -0.75);
- color: var(--text-subdued);
-}
-@media (max-width: 767px) {
- .workflow {
- grid-template-columns: 1fr;
- }
- .connector {
- display: none;
- }
-}
diff --git a/homepage/src/components/marketing/Workflow.tsx b/homepage/src/components/marketing/Workflow.tsx
deleted file mode 100644
index 9edbecb..0000000
--- a/homepage/src/components/marketing/Workflow.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import { BidiText } from '@/components/foundation/BidiText';
-import { Icon, type ApprovedIconName } from '@/components/foundation/Icon';
-import { Heading, Text, type HeadingLevel } from '@/components/typography/Typography';
-import styles from './Workflow.module.css';
-
-export interface WorkflowItem {
- id: string;
- ordinal: string;
- title: string;
- description: string;
- icon?: ApprovedIconName;
-}
-
-interface WorkflowProps {
- items: WorkflowItem[];
- label: string;
- headingLevel?: HeadingLevel;
-}
-
-export function Workflow({ items, label, headingLevel = 3 }: WorkflowProps) {
- return (
-
- {items.map((item, index) => (
- -
-
- {item.icon ? : {item.ordinal}}
-
-
-
- {item.title}
-
- {item.description}
-
- {index < items.length - 1 ? (
-
-
-
- ) : null}
-
- ))}
-
- );
-}
diff --git a/homepage/src/components/media/MediaFrame.module.css b/homepage/src/components/media/MediaFrame.module.css
deleted file mode 100644
index 00eb591..0000000
--- a/homepage/src/components/media/MediaFrame.module.css
+++ /dev/null
@@ -1,39 +0,0 @@
-.figure {
- display: grid;
- gap: var(--space-2);
- margin: 0;
-}
-.frame {
- position: relative;
- display: grid;
- place-items: center;
- min-inline-size: 0;
- overflow: hidden;
- border: 1px solid var(--border-standard);
- border-radius: var(--radius-md);
- background: var(--surface-muted);
-}
-.square {
- aspect-ratio: 1;
-}
-.landscape {
- aspect-ratio: 4 / 3;
-}
-.wide {
- aspect-ratio: 16 / 9;
-}
-.portrait {
- aspect-ratio: 3 / 4;
-}
-.auto {
- aspect-ratio: auto;
-}
-.image {
- inline-size: 100%;
- block-size: 100%;
- object-fit: cover;
-}
-.figure figcaption {
- color: var(--text-secondary);
- font-size: var(--type-caption-size);
-}
diff --git a/homepage/src/components/media/MediaFrame.tsx b/homepage/src/components/media/MediaFrame.tsx
deleted file mode 100644
index 831cddf..0000000
--- a/homepage/src/components/media/MediaFrame.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import { classNames } from '@/lib/components/classNames';
-import Image, { type ImageProps } from 'next/image';
-import type { ReactNode } from 'react';
-import styles from './MediaFrame.module.css';
-
-export type MediaRatio = 'square' | 'landscape' | 'wide' | 'portrait' | 'auto';
-
-interface MediaFrameProps {
- ratio?: MediaRatio;
- children: ReactNode;
- caption?: string;
- decorative?: boolean;
- className?: string;
-}
-
-export function MediaFrame({
- ratio = 'landscape',
- children,
- caption,
- decorative = false,
- className,
-}: MediaFrameProps) {
- return (
-
- {children}
- {caption ? {caption} : null}
-
- );
-}
-
-interface ResponsiveImageProps extends Omit {
- ratio?: MediaRatio;
- caption?: string;
-}
-
-export function ResponsiveImage({
- ratio = 'landscape',
- caption,
- alt,
- ...props
-}: ResponsiveImageProps) {
- return (
-
-
-
- );
-}
diff --git a/homepage/src/components/navigation/Breadcrumbs.module.css b/homepage/src/components/navigation/Breadcrumbs.module.css
deleted file mode 100644
index d4cfed8..0000000
--- a/homepage/src/components/navigation/Breadcrumbs.module.css
+++ /dev/null
@@ -1,18 +0,0 @@
-.nav ol {
- display: flex;
- flex-wrap: wrap;
- gap: var(--space-2);
- margin: 0;
- padding: 0;
- list-style: none;
-}
-.nav li {
- display: inline-flex;
- align-items: center;
- gap: var(--space-2);
- color: var(--text-secondary);
- font-size: var(--type-label-size);
-}
-.separator {
- color: var(--text-subdued);
-}
diff --git a/homepage/src/components/navigation/Breadcrumbs.tsx b/homepage/src/components/navigation/Breadcrumbs.tsx
deleted file mode 100644
index 770e2d0..0000000
--- a/homepage/src/components/navigation/Breadcrumbs.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import { ActionLink } from '@/components/actions/ActionLink';
-import { Icon } from '@/components/foundation/Icon';
-import styles from './Breadcrumbs.module.css';
-
-export interface BreadcrumbItem {
- label: string;
- href?: string;
-}
-
-export function Breadcrumbs({ label, items }: { label: string; items: BreadcrumbItem[] }) {
- return (
-
- );
-}
diff --git a/homepage/src/components/navigation/FooterLinkGroup.module.css b/homepage/src/components/navigation/FooterLinkGroup.module.css
deleted file mode 100644
index 2e5251e..0000000
--- a/homepage/src/components/navigation/FooterLinkGroup.module.css
+++ /dev/null
@@ -1,11 +0,0 @@
-.group h2 {
- margin: 0;
- font-size: var(--type-body-size);
-}
-.group ul {
- display: grid;
- gap: var(--space-2);
- margin: var(--space-3) 0 0;
- padding: 0;
- list-style: none;
-}
diff --git a/homepage/src/components/navigation/FooterLinkGroup.tsx b/homepage/src/components/navigation/FooterLinkGroup.tsx
deleted file mode 100644
index 0783a58..0000000
--- a/homepage/src/components/navigation/FooterLinkGroup.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import { ActionLink } from '@/components/actions/ActionLink';
-import styles from './FooterLinkGroup.module.css';
-
-export interface FooterLinkItem {
- id: string;
- label: string;
- href?: string;
- disabledReason?: string;
-}
-
-export function FooterLinkGroup({ title, links }: { title: string; links: FooterLinkItem[] }) {
- return (
-
- {title}
-
- {links.map((link) => (
- -
-
- {link.label}
-
-
- ))}
-
-
- );
-}
diff --git a/homepage/src/components/navigation/SectionNavigation.module.css b/homepage/src/components/navigation/SectionNavigation.module.css
deleted file mode 100644
index e2e52d3..0000000
--- a/homepage/src/components/navigation/SectionNavigation.module.css
+++ /dev/null
@@ -1,20 +0,0 @@
-.nav {
- overflow-x: auto;
- border-block-end: 1px solid var(--border-standard);
-}
-.nav ul {
- display: flex;
- inline-size: max-content;
- min-inline-size: 100%;
- gap: var(--space-6);
- margin: 0;
- padding: 0;
- list-style: none;
-}
-.nav a {
- border-block-end: 3px solid transparent;
-}
-.nav a[aria-current='location'] {
- border-block-end-color: var(--interactive-primary);
- color: var(--text-primary);
-}
diff --git a/homepage/src/components/navigation/SectionNavigation.tsx b/homepage/src/components/navigation/SectionNavigation.tsx
deleted file mode 100644
index ba09602..0000000
--- a/homepage/src/components/navigation/SectionNavigation.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { ActionLink } from '@/components/actions/ActionLink';
-import styles from './SectionNavigation.module.css';
-
-export interface SectionNavigationItem {
- id: string;
- label: string;
- href: string;
- current?: boolean;
-}
-
-export function SectionNavigation({
- label,
- items,
-}: {
- label: string;
- items: SectionNavigationItem[];
-}) {
- return (
-
- );
-}
diff --git a/homepage/src/components/overlays/Dialog.module.css b/homepage/src/components/overlays/Dialog.module.css
deleted file mode 100644
index 59ace84..0000000
--- a/homepage/src/components/overlays/Dialog.module.css
+++ /dev/null
@@ -1,71 +0,0 @@
-.dialog {
- max-inline-size: none;
- max-block-size: none;
- margin: auto;
- padding: 0;
- border: 0;
- background: transparent;
- color: var(--text-primary);
- overflow: visible;
-}
-.dialog::backdrop {
- background: var(--overlay-scrim);
-}
-.panel {
- display: grid;
- max-block-size: calc(100dvh - var(--space-8));
- overflow: hidden;
- border: 1px solid var(--border-standard);
- border-radius: var(--radius-md);
- background: var(--surface-elevated);
- box-shadow: var(--shadow-overlay);
-}
-.modal .panel {
- inline-size: min(44rem, calc(100vw - var(--space-8)));
-}
-.drawer {
- margin-block: 0;
- margin-inline-start: auto;
- margin-inline-end: 0;
-}
-.drawer .panel {
- inline-size: min(24rem, 88vw);
- min-block-size: 100dvh;
- border-radius: 0;
-}
-.header {
- display: flex;
- align-items: flex-start;
- justify-content: space-between;
- gap: var(--space-4);
- padding: var(--space-6);
- border-block-end: 1px solid var(--border-standard);
-}
-.header h2 {
- margin: 0;
- font-size: var(--type-heading-3-size);
-}
-.header p {
- margin-block: var(--space-2) 0;
- color: var(--text-secondary);
-}
-.content {
- min-block-size: 0;
- padding: var(--space-6);
- overflow-y: auto;
-}
-@media (max-width: 479px) {
- .modal .panel {
- inline-size: calc(100vw - var(--space-4));
- max-block-size: calc(100dvh - var(--space-4));
- }
- .header,
- .content {
- padding: var(--space-4);
- }
-}
-@media (forced-colors: active) {
- .panel {
- border: 2px solid CanvasText;
- }
-}
diff --git a/homepage/src/components/overlays/Dialog.tsx b/homepage/src/components/overlays/Dialog.tsx
deleted file mode 100644
index 8d4323d..0000000
--- a/homepage/src/components/overlays/Dialog.tsx
+++ /dev/null
@@ -1,99 +0,0 @@
-'use client';
-
-import { Button } from '@/components/actions/Button';
-import { useScrollLock } from '@/components/foundation/client-hooks';
-import { classNames } from '@/lib/components/classNames';
-import { useEffect, useId, useRef, type ReactNode, type RefObject } from 'react';
-import styles from './Dialog.module.css';
-
-interface DialogProps {
- open: boolean;
- onOpenChange: (open: boolean) => void;
- title: string;
- description?: string;
- children: ReactNode;
- closeLabel: string;
- initialFocusRef?: RefObject;
- returnFocusRef?: RefObject;
- variant?: 'modal' | 'drawer';
- dismissible?: boolean;
- className?: string;
-}
-
-export function Dialog({
- open,
- onOpenChange,
- title,
- description,
- children,
- closeLabel,
- initialFocusRef,
- returnFocusRef,
- variant = 'modal',
- dismissible = true,
- className,
-}: DialogProps) {
- const dialogRef = useRef(null);
- const closeRef = useRef(null);
- const titleId = useId();
- const descriptionId = useId();
- useScrollLock(open);
-
- useEffect(() => {
- const dialog = dialogRef.current;
- if (!dialog) return;
- if (open && !dialog.open) {
- dialog.showModal();
- window.requestAnimationFrame(() => (initialFocusRef?.current ?? closeRef.current)?.focus());
- } else if (!open && dialog.open) {
- dialog.close();
- }
- }, [initialFocusRef, open]);
-
- const close = () => {
- dialogRef.current?.close();
- onOpenChange(false);
- window.requestAnimationFrame(() => returnFocusRef?.current?.focus());
- };
-
- return (
-
- );
-}
diff --git a/homepage/src/components/overlays/DropdownMenu.module.css b/homepage/src/components/overlays/DropdownMenu.module.css
deleted file mode 100644
index 143a549..0000000
--- a/homepage/src/components/overlays/DropdownMenu.module.css
+++ /dev/null
@@ -1,39 +0,0 @@
-.root {
- position: relative;
- display: inline-block;
-}
-.menu {
- position: absolute;
- z-index: var(--layer-overlay);
- inset-block-start: calc(100% + var(--space-2));
- inset-inline-end: 0;
- display: grid;
- min-inline-size: 13rem;
- padding: var(--space-2);
- border: 1px solid var(--border-standard);
- border-radius: var(--radius-sm);
- background: var(--surface-elevated);
- box-shadow: var(--shadow-overlay);
-}
-.item {
- min-block-size: 2.75rem;
- padding-block: var(--space-2);
- padding-inline: var(--space-3);
- border: 0;
- border-radius: calc(var(--radius-sm) - 2px);
- background: transparent;
- color: var(--text-primary);
- cursor: pointer;
- text-align: start;
-}
-.item:hover:not(:disabled),
-.item:focus-visible {
- background: var(--surface-muted);
-}
-.item:disabled {
- cursor: not-allowed;
- opacity: var(--opacity-disabled);
-}
-.destructive {
- color: var(--status-error-text);
-}
diff --git a/homepage/src/components/overlays/DropdownMenu.tsx b/homepage/src/components/overlays/DropdownMenu.tsx
deleted file mode 100644
index 31fba3f..0000000
--- a/homepage/src/components/overlays/DropdownMenu.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-'use client';
-
-import { Button } from '@/components/actions/Button';
-import { useEscapeKey, useOutsideClick } from '@/components/foundation/client-hooks';
-import { classNames } from '@/lib/components/classNames';
-import { useRef, useState, type KeyboardEvent } from 'react';
-import styles from './DropdownMenu.module.css';
-
-export interface MenuItem {
- id: string;
- label: string;
- onSelect: () => void;
- disabled?: boolean;
- destructive?: boolean;
-}
-
-interface DropdownMenuProps {
- label: string;
- items: MenuItem[];
- className?: string;
-}
-
-export function DropdownMenu({ label, items, className }: DropdownMenuProps) {
- const [open, setOpen] = useState(false);
- const rootRef = useRef(null);
- const itemRefs = useRef>([]);
- useOutsideClick(rootRef, () => setOpen(false), open);
- useEscapeKey(() => setOpen(false), open);
-
- const focusItem = (index: number) => {
- const enabled = items
- .map((item, itemIndex) => ({ item, itemIndex }))
- .filter(({ item }) => !item.disabled);
- const target = enabled[(index + enabled.length) % enabled.length];
- if (target) itemRefs.current[target.itemIndex]?.focus();
- };
-
- const handleKeyDown = (event: KeyboardEvent) => {
- const currentIndex = itemRefs.current.findIndex((node) => node === document.activeElement);
- if (event.key === 'ArrowDown') {
- event.preventDefault();
- focusItem(currentIndex + 1);
- } else if (event.key === 'ArrowUp') {
- event.preventDefault();
- focusItem(currentIndex - 1);
- } else if (event.key === 'Home') {
- event.preventDefault();
- focusItem(0);
- } else if (event.key === 'End') {
- event.preventDefault();
- focusItem(items.length - 1);
- }
- };
-
- return (
-
-
- {open ? (
-
- {items.map((item, index) => (
-
- ))}
-
- ) : null}
-
- );
-}
diff --git a/homepage/src/components/overlays/Tooltip.module.css b/homepage/src/components/overlays/Tooltip.module.css
deleted file mode 100644
index 046e648..0000000
--- a/homepage/src/components/overlays/Tooltip.module.css
+++ /dev/null
@@ -1,41 +0,0 @@
-.root {
- position: relative;
- display: inline-flex;
-}
-.tooltip {
- position: absolute;
- z-index: var(--layer-overlay);
- inset-block-end: calc(100% + var(--space-2));
- inset-inline-start: 50%;
- inline-size: max-content;
- max-inline-size: 16rem;
- padding-block: var(--space-2);
- padding-inline: var(--space-3);
- border-radius: var(--radius-sm);
- background: var(--surface-strong);
- color: var(--text-primary);
- font-size: var(--type-caption-size);
- opacity: 0;
- pointer-events: none;
- transform: translateX(-50%) translateY(var(--space-1));
- transition:
- opacity var(--duration-fast) var(--easing-productive),
- transform var(--duration-fast) var(--easing-productive);
-}
-.root:hover .tooltip,
-.root:focus-within .tooltip {
- opacity: 1;
- transform: translateX(-50%) translateY(0);
-}
-html[dir='rtl'] .tooltip {
- transform: translateX(50%) translateY(var(--space-1));
-}
-html[dir='rtl'] .root:hover .tooltip,
-html[dir='rtl'] .root:focus-within .tooltip {
- transform: translateX(50%) translateY(0);
-}
-@media (prefers-reduced-motion: reduce) {
- .tooltip {
- transition: none;
- }
-}
diff --git a/homepage/src/components/overlays/Tooltip.tsx b/homepage/src/components/overlays/Tooltip.tsx
deleted file mode 100644
index 98062ba..0000000
--- a/homepage/src/components/overlays/Tooltip.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import { classNames } from '@/lib/components/classNames';
-import type { ReactNode } from 'react';
-import styles from './Tooltip.module.css';
-
-export function Tooltip({
- content,
- children,
- className,
-}: {
- content: string;
- children: ReactNode;
- className?: string;
-}) {
- return (
-
- {children}
-
- {content}
-
-
- );
-}
diff --git a/homepage/src/components/surfaces/Callout.module.css b/homepage/src/components/surfaces/Callout.module.css
deleted file mode 100644
index 6ad7d3a..0000000
--- a/homepage/src/components/surfaces/Callout.module.css
+++ /dev/null
@@ -1,16 +0,0 @@
-.callout {
- display: grid;
- grid-template-columns: auto 1fr;
- gap: var(--space-4);
- padding: var(--space-5);
- border-inline-start: 4px solid var(--interactive-primary);
- background: var(--status-info-surface);
- color: var(--status-info-text);
-}
-.callout strong {
- display: block;
- margin-block-end: var(--space-1);
-}
-.callout :last-child {
- margin-block-end: 0;
-}
diff --git a/homepage/src/components/surfaces/Callout.tsx b/homepage/src/components/surfaces/Callout.tsx
deleted file mode 100644
index dde932d..0000000
--- a/homepage/src/components/surfaces/Callout.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import { Icon, type ApprovedIconName } from '@/components/foundation/Icon';
-import type { ReactNode } from 'react';
-import styles from './Callout.module.css';
-
-export function Callout({
- icon = 'info',
- title,
- children,
-}: {
- icon?: ApprovedIconName;
- title: string;
- children: ReactNode;
-}) {
- return (
-
- );
-}
diff --git a/homepage/src/components/surfaces/Card.module.css b/homepage/src/components/surfaces/Card.module.css
deleted file mode 100644
index 1bfd137..0000000
--- a/homepage/src/components/surfaces/Card.module.css
+++ /dev/null
@@ -1,50 +0,0 @@
-.card {
- min-inline-size: 0;
- border: 1px solid var(--border-standard);
- border-radius: var(--radius-md);
-}
-.default {
- background: var(--surface-primary);
-}
-.muted {
- background: var(--surface-muted);
-}
-.elevated {
- background: var(--surface-elevated);
- box-shadow: var(--shadow-card);
-}
-.strong {
- background: var(--surface-strong);
-}
-.none {
- padding: 0;
-}
-.compact {
- padding: var(--space-4);
-}
-.comfortable {
- padding: var(--space-6);
-}
-.spacious {
- padding: clamp(var(--space-6), 5vw, var(--space-10));
-}
-.interactive {
- transition:
- border-color var(--duration-fast) var(--easing-productive),
- transform var(--duration-fast) var(--easing-productive);
-}
-.interactive:hover {
- border-color: var(--interactive-primary);
- transform: translateY(-2px);
-}
-.interactive:focus-within {
- border-color: var(--focus-ring);
-}
-@media (prefers-reduced-motion: reduce) {
- .interactive {
- transition: none;
- }
- .interactive:hover {
- transform: none;
- }
-}
diff --git a/homepage/src/components/surfaces/Card.tsx b/homepage/src/components/surfaces/Card.tsx
deleted file mode 100644
index a2b5fc0..0000000
--- a/homepage/src/components/surfaces/Card.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import { classNames } from '@/lib/components/classNames';
-import type { HTMLAttributes, ReactNode } from 'react';
-import styles from './Card.module.css';
-
-export type SurfaceTone = 'default' | 'muted' | 'elevated' | 'strong';
-export type SurfacePadding = 'none' | 'compact' | 'comfortable' | 'spacious';
-
-interface CardProps extends HTMLAttributes {
- as?: 'article' | 'section' | 'div';
- tone?: SurfaceTone;
- padding?: SurfacePadding;
- interactive?: boolean;
- children: ReactNode;
-}
-
-export function Card({
- as: Component = 'article',
- tone = 'default',
- padding = 'comfortable',
- interactive = false,
- className,
- children,
- ...props
-}: CardProps) {
- return (
-
- {children}
-
- );
-}
-
-export function Panel(props: Omit) {
- return ;
-}
diff --git a/homepage/src/components/typography/Typography.module.css b/homepage/src/components/typography/Typography.module.css
deleted file mode 100644
index 4606fbb..0000000
--- a/homepage/src/components/typography/Typography.module.css
+++ /dev/null
@@ -1,70 +0,0 @@
-.heading {
- margin: 0;
- color: var(--text-primary);
-}
-.display {
- max-inline-size: 16ch;
- font-size: var(--type-display-size);
- line-height: var(--line-display);
- letter-spacing: var(--tracking-display);
-}
-.section {
- font-size: var(--type-heading-2-size);
-}
-.subheading {
- font-size: var(--type-heading-3-size);
-}
-.eyebrow {
- margin: 0;
- color: var(--interactive-primary);
- font-size: var(--type-label-size);
- font-weight: 800;
- letter-spacing: var(--tracking-label);
- text-transform: uppercase;
-}
-html[dir='rtl'] .eyebrow {
- letter-spacing: normal;
- text-transform: none;
-}
-.text {
- margin: 0;
-}
-.lead {
- max-inline-size: 62ch;
- color: var(--text-secondary);
- font-size: var(--type-body-large-size);
-}
-.body {
- max-inline-size: 70ch;
-}
-.supporting {
- max-inline-size: 70ch;
- color: var(--text-secondary);
-}
-.caption {
- color: var(--text-secondary);
- font-size: var(--type-caption-size);
-}
-.label {
- font-size: var(--type-label-size);
- font-weight: 700;
-}
-.quote {
- display: grid;
- gap: var(--space-3);
- margin: 0;
- padding-inline-start: var(--space-6);
- border-inline-start: 4px solid var(--interactive-primary);
-}
-.quote blockquote {
- margin: 0;
- font-size: var(--type-body-large-size);
-}
-.quote figcaption {
- color: var(--text-secondary);
- font-size: var(--type-label-size);
-}
-html[lang='ar'] .display {
- letter-spacing: normal;
- line-height: 1.25;
-}
diff --git a/homepage/src/components/typography/Typography.tsx b/homepage/src/components/typography/Typography.tsx
deleted file mode 100644
index 8fd8c44..0000000
--- a/homepage/src/components/typography/Typography.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import { classNames } from '@/lib/components/classNames';
-import type { HTMLAttributes, ReactNode } from 'react';
-import styles from './Typography.module.css';
-
-export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
-export type HeadingStyle = 'display' | 'section' | 'subheading';
-
-interface HeadingProps extends HTMLAttributes {
- level: HeadingLevel;
- appearance?: HeadingStyle;
- children: ReactNode;
-}
-
-export function Heading({
- level,
- appearance = 'section',
- className,
- children,
- ...props
-}: HeadingProps) {
- const Component = `h${level}` as const;
- return (
-
- {children}
-
- );
-}
-
-export function Eyebrow({ children, className, ...props }: HTMLAttributes) {
- return (
-
- {children}
-
- );
-}
-
-export type TextVariant = 'lead' | 'body' | 'supporting' | 'caption' | 'label';
-
-interface TextProps extends HTMLAttributes {
- variant?: TextVariant;
- children: ReactNode;
-}
-
-export function Text({ variant = 'body', className, children, ...props }: TextProps) {
- return (
-
- {children}
-
- );
-}
-
-export function Quote({
- children,
- cite,
- className,
-}: {
- children: ReactNode;
- cite?: string;
- className?: string;
-}) {
- return (
-
- {children}
- {cite ? {cite} : null}
-
- );
-}
diff --git a/homepage/src/content/development/component-lab.ts b/homepage/src/content/development/component-lab.ts
deleted file mode 100644
index 5f9b587..0000000
--- a/homepage/src/content/development/component-lab.ts
+++ /dev/null
@@ -1,373 +0,0 @@
-import type { Locale } from '@/lib/localization/config';
-
-export interface ComponentLabCopy {
- developmentLabel: string;
- title: string;
- body: string;
- actions: { primary: string; conversion: string; loading: string; disabled: string };
- form: {
- label: string;
- supporting: string;
- placeholder: string;
- error: string;
- checkbox: string;
- switch: string;
- };
- tabs: { label: string; items: Array<{ id: string; label: string; content: string }> };
- accordion: { items: Array<{ id: string; title: string; content: string }> };
- alert: { title: string; body: string };
- workflow: Array<{ id: string; ordinal: string; title: string; description: string }>;
- comparison: { beforeTitle: string; afterTitle: string; before: string[]; after: string[] };
- preview: {
- title: string;
- caption: string;
- illustrative: string;
- rows: Array<{ id: string; primary: string; secondary: string; status: string }>;
- };
- metric: { value: string; label: string; qualification: string; status: string };
- cta: { title: string; body: string; primary: string; unavailable: string };
- dialog: { open: string; title: string; description: string; close: string; content: string };
- menu: { label: string; first: string; second: string };
-}
-
-const copy = {
- en: {
- developmentLabel: 'Development fixture, not production content',
- title: 'Core component system',
- body: 'A deterministic test surface for accessible, localized, theme-aware, and RTL-native components.',
- actions: {
- primary: 'Primary action',
- conversion: 'Create account',
- loading: 'Submitting',
- disabled: 'Unavailable action',
- },
- form: {
- label: 'Work email',
- supporting: 'Use a fictional address in this development fixture.',
- placeholder: 'name@fictional.invalid',
- error: 'Enter an address in the required format.',
- checkbox: 'Send a fictional confirmation',
- switch: 'Enable compact preview',
- },
- tabs: {
- label: 'Operations views',
- items: [
- { id: 'reservations', label: 'Reservations', content: 'Reservation fixture content.' },
- {
- id: 'fleet',
- label: 'Fleet availability',
- content: 'Fleet fixture content with intentionally expanded wording.',
- },
- { id: 'returns', label: 'Returns', content: 'Return inspection fixture content.' },
- ],
- },
- accordion: {
- items: [
- {
- id: 'a1',
- title: 'How does the component system handle localization?',
- content:
- 'Copy is passed as complete localized values rather than concatenated fragments.',
- },
- {
- id: 'a2',
- title: 'Does this submit data?',
- content: 'No. External submission remains blocked pending CRM and legal approval.',
- },
- ],
- },
- alert: {
- title: 'Development-only fixture',
- body: 'This surface demonstrates behavior without publishing customer evidence or operational claims.',
- },
- workflow: [
- {
- id: 'reserve',
- ordinal: '1',
- title: 'Reservation',
- description: 'Capture an illustrative reservation request.',
- },
- {
- id: 'prepare',
- ordinal: '2',
- title: 'Preparation',
- description: 'Review a fictional vehicle readiness state.',
- },
- {
- id: 'handover',
- ordinal: '3',
- title: 'Handover',
- description: 'Present a controlled pickup checklist.',
- },
- {
- id: 'return',
- ordinal: '4',
- title: 'Return',
- description: 'Record an illustrative inspection outcome.',
- },
- ],
- comparison: {
- beforeTitle: 'Fragmented operations',
- afterTitle: 'Connected workflow',
- before: ['Repeated manual entry', 'Scattered status checks', 'Unclear ownership'],
- after: ['One shared workflow', 'Visible state changes', 'Explicit responsibility'],
- },
- preview: {
- title: 'Fleet availability preview',
- caption: 'Illustrative interface fixture. It is not live operational data.',
- illustrative: 'Illustrative data',
- rows: [
- { id: 'RDG-1042', primary: 'Compact vehicle', secondary: 'RDG-1042', status: 'Available' },
- { id: 'RDG-2088', primary: 'Utility vehicle', secondary: 'RDG-2088', status: 'Preparing' },
- ],
- },
- metric: {
- value: '24',
- label: 'Illustrative workflow checkpoints',
- qualification: 'Development fixture only. Not a public performance claim.',
- status: 'Research only',
- },
- cta: {
- title: 'CTA destination remains governed',
- body: 'The conversion treatment is reusable, while unresolved destinations stay disabled.',
- primary: 'Create account',
- unavailable: 'Destination pending approval',
- },
- dialog: {
- open: 'Open dialog',
- title: 'Accessible dialog fixture',
- description: 'Focus moves into this native modal and returns to the trigger.',
- close: 'Close dialog',
- content: 'No data is submitted from this component fixture.',
- },
- menu: { label: 'More actions', first: 'Review fixture', second: 'Archive fixture' },
- },
- fr: {
- developmentLabel: 'Exemple de développement, contenu non destiné à la production',
- title: 'Système de composants principal',
- body: 'Une surface de test déterministe pour des composants accessibles, localisés, adaptés aux thèmes et natifs en RTL.',
- actions: {
- primary: 'Action principale',
- conversion: 'Créer un compte',
- loading: 'Envoi en cours',
- disabled: 'Action indisponible',
- },
- form: {
- label: 'Adresse e-mail professionnelle',
- supporting: 'Utilisez une adresse fictive dans cet exemple de développement.',
- placeholder: 'nom@fictif.invalid',
- error: 'Saisissez une adresse au format requis.',
- checkbox: 'Envoyer une confirmation fictive',
- switch: 'Activer l’aperçu compact',
- },
- tabs: {
- label: 'Vues des opérations',
- items: [
- { id: 'reservations', label: 'Réservations', content: 'Contenu fictif des réservations.' },
- {
- id: 'fleet',
- label: 'Disponibilité du parc automobile',
- content: 'Contenu fictif du parc avec un libellé volontairement plus long.',
- },
- { id: 'returns', label: 'Retours', content: 'Contenu fictif de l’inspection au retour.' },
- ],
- },
- accordion: {
- items: [
- {
- id: 'a1',
- title: 'Comment le système gère-t-il la localisation ?',
- content:
- 'Le texte est transmis sous forme de valeurs localisées complètes, sans concaténation de fragments.',
- },
- {
- id: 'a2',
- title: 'Ces composants transmettent-ils des données ?',
- content:
- 'Non. La transmission externe reste bloquée jusqu’à l’approbation du CRM et du contenu juridique.',
- },
- ],
- },
- alert: {
- title: 'Exemple réservé au développement',
- body: 'Cette surface démontre les comportements sans publier de preuves client ni d’allégations opérationnelles.',
- },
- workflow: [
- {
- id: 'reserve',
- ordinal: '1',
- title: 'Réservation',
- description: 'Saisir une demande de réservation illustrative.',
- },
- {
- id: 'prepare',
- ordinal: '2',
- title: 'Préparation',
- description: 'Examiner un état fictif de préparation du véhicule.',
- },
- {
- id: 'handover',
- ordinal: '3',
- title: 'Remise du véhicule',
- description: 'Présenter une liste de contrôle maîtrisée.',
- },
- {
- id: 'return',
- ordinal: '4',
- title: 'Retour',
- description: 'Consigner un résultat d’inspection illustratif.',
- },
- ],
- comparison: {
- beforeTitle: 'Opérations fragmentées',
- afterTitle: 'Flux de travail connecté',
- before: ['Saisie manuelle répétée', 'Vérifications dispersées', 'Responsabilités imprécises'],
- after: ['Un flux partagé', 'Des changements d’état visibles', 'Une responsabilité explicite'],
- },
- preview: {
- title: 'Aperçu de la disponibilité du parc',
- caption:
- 'Exemple d’interface illustratif. Il ne s’agit pas de données opérationnelles en direct.',
- illustrative: 'Données illustratives',
- rows: [
- {
- id: 'RDG-1042',
- primary: 'Véhicule compact',
- secondary: 'RDG-1042',
- status: 'Disponible',
- },
- {
- id: 'RDG-2088',
- primary: 'Véhicule utilitaire',
- secondary: 'RDG-2088',
- status: 'En préparation',
- },
- ],
- },
- metric: {
- value: '24',
- label: 'Points de contrôle illustratifs',
- qualification: 'Exemple de développement uniquement. Ce n’est pas une allégation publique.',
- status: 'Recherche uniquement',
- },
- cta: {
- title: 'La destination du CTA reste contrôlée',
- body: 'Le traitement de conversion est réutilisable, tandis que les destinations non résolues restent désactivées.',
- primary: 'Créer un compte',
- unavailable: 'Destination en attente d’approbation',
- },
- dialog: {
- open: 'Ouvrir la boîte de dialogue',
- title: 'Exemple de boîte de dialogue accessible',
- description: 'Le focus entre dans la fenêtre modale native puis revient au déclencheur.',
- close: 'Fermer la boîte de dialogue',
- content: 'Aucune donnée n’est transmise depuis cet exemple.',
- },
- menu: { label: 'Autres actions', first: 'Examiner l’exemple', second: 'Archiver l’exemple' },
- },
- ar: {
- developmentLabel: 'نموذج للتطوير وليس محتوى للإنتاج',
- title: 'نظام المكوّنات الأساسي',
- body: 'سطح اختبار ثابت لمكوّنات قابلة للوصول ومترجمة ومتوافقة مع السمات وداعمة لاتجاه الكتابة من اليمين إلى اليسار.',
- actions: {
- primary: 'إجراء أساسي',
- conversion: 'حجز عرض توضيحي',
- loading: 'جارٍ الإرسال',
- disabled: 'إجراء غير متاح',
- },
- form: {
- label: 'البريد الإلكتروني للعمل',
- supporting: 'استخدم عنواناً افتراضياً في نموذج التطوير هذا.',
- placeholder: 'name@fictional.invalid',
- error: 'أدخل عنواناً بالتنسيق المطلوب.',
- checkbox: 'إرسال تأكيد افتراضي',
- switch: 'تفعيل المعاينة المدمجة',
- },
- tabs: {
- label: 'عروض العمليات',
- items: [
- { id: 'reservations', label: 'الحجوزات', content: 'محتوى افتراضي للحجوزات.' },
- {
- id: 'fleet',
- label: 'توفر أسطول المركبات',
- content: 'محتوى افتراضي للأسطول مع نص موسّع عمداً.',
- },
- { id: 'returns', label: 'عمليات الإرجاع', content: 'محتوى افتراضي لفحص الإرجاع.' },
- ],
- },
- accordion: {
- items: [
- {
- id: 'a1',
- title: 'كيف يتعامل نظام المكوّنات مع الترجمة؟',
- content: 'يُمرر النص كقيم مترجمة كاملة بدلاً من تجميع أجزاء الجمل.',
- },
- {
- id: 'a2',
- title: 'هل ترسل هذه المكوّنات بيانات؟',
- content:
- 'لا. يظل الإرسال الخارجي محظوراً إلى حين اعتماد نظام إدارة علاقات العملاء والمحتوى القانوني.',
- },
- ],
- },
- alert: {
- title: 'نموذج مخصص للتطوير',
- body: 'يوضح هذا السطح السلوك من دون نشر أدلة عملاء أو ادعاءات تشغيلية.',
- },
- workflow: [
- { id: 'reserve', ordinal: '١', title: 'الحجز', description: 'تسجيل طلب حجز توضيحي.' },
- {
- id: 'prepare',
- ordinal: '٢',
- title: 'تجهيز المركبة',
- description: 'مراجعة حالة افتراضية لجاهزية المركبة.',
- },
- {
- id: 'handover',
- ordinal: '٣',
- title: 'التسليم',
- description: 'عرض قائمة تحقق مضبوطة للاستلام.',
- },
- { id: 'return', ordinal: '٤', title: 'الإرجاع', description: 'تسجيل نتيجة فحص توضيحية.' },
- ],
- comparison: {
- beforeTitle: 'عمليات مجزأة',
- afterTitle: 'سير عمل مترابط',
- before: ['إدخال يدوي متكرر', 'فحوصات حالة متفرقة', 'مسؤولية غير واضحة'],
- after: ['سير عمل مشترك', 'تغييرات حالة مرئية', 'مسؤولية محددة'],
- },
- preview: {
- title: 'معاينة توفر الأسطول',
- caption: 'نموذج واجهة توضيحي وليس بيانات تشغيلية مباشرة.',
- illustrative: 'بيانات توضيحية',
- rows: [
- { id: 'RDG-1042', primary: 'مركبة مدمجة', secondary: 'RDG-1042', status: 'متاحة' },
- { id: 'RDG-2088', primary: 'مركبة خدمية', secondary: 'RDG-2088', status: 'قيد التجهيز' },
- ],
- },
- metric: {
- value: '٢٤',
- label: 'نقاط تحقق توضيحية لسير العمل',
- qualification: 'نموذج تطوير فقط وليس ادعاءً عاماً عن الأداء.',
- status: 'للبحث فقط',
- },
- cta: {
- title: 'تظل وجهة الدعوة إلى الإجراء خاضعة للحوكمة',
- body: 'يمكن إعادة استخدام نمط التحويل، بينما تبقى الوجهات غير المحسومة معطلة.',
- primary: 'حجز عرض توضيحي',
- unavailable: 'الوجهة بانتظار الاعتماد',
- },
- dialog: {
- open: 'فتح مربع الحوار',
- title: 'نموذج مربع حوار قابل للوصول',
- description: 'ينتقل التركيز إلى النافذة الأصلية ثم يعود إلى زر الفتح.',
- close: 'إغلاق مربع الحوار',
- content: 'لا تُرسل أي بيانات من نموذج المكوّن هذا.',
- },
- menu: { label: 'إجراءات إضافية', first: 'مراجعة النموذج', second: 'أرشفة النموذج' },
- },
-} satisfies Record;
-
-export function getComponentLabCopy(locale: Locale): ComponentLabCopy {
- return copy[locale];
-}
diff --git a/homepage/src/content/homepage-model.ts b/homepage/src/content/homepage-model.ts
deleted file mode 100644
index 280a4a4..0000000
--- a/homepage/src/content/homepage-model.ts
+++ /dev/null
@@ -1,273 +0,0 @@
-import type { ApprovedIconName } from '@/components/foundation/Icon';
-import type { PreviewRow } from '@/components/marketing/ProductPreview';
-import type { HomepageMessages, ShellMessages } from '@/lib/localization/messages';
-
-export interface HomepageAction {
- label: string;
- disabledReason: string;
-}
-
-export interface TitledDescription {
- id: string;
- title: string;
- description: string;
- icon?: ApprovedIconName;
-}
-
-export interface HomepageWorkflowStep extends TitledDescription {
- ordinal: string;
-}
-
-export interface HomepageFaqItem {
- id: string;
- title: string;
- body: string;
-}
-
-export interface HomepageContent {
- hero: {
- eyebrow: string;
- title: string;
- body: string;
- primary: HomepageAction;
- secondary: HomepageAction;
- preview: {
- title: string;
- caption: string;
- illustrativeLabel: string;
- rows: PreviewRow[];
- };
- };
- trust: {
- eyebrow: string;
- title: string;
- body: string;
- items: TitledDescription[];
- };
- comparison: {
- eyebrow: string;
- title: string;
- body: string;
- beforeTitle: string;
- beforeItems: string[];
- afterTitle: string;
- afterItems: string[];
- };
- workflow: {
- eyebrow: string;
- title: string;
- body: string;
- steps: HomepageWorkflowStep[];
- };
- roles: {
- eyebrow: string;
- title: string;
- items: TitledDescription[];
- };
- modules: {
- eyebrow: string;
- title: string;
- body: string;
- items: TitledDescription[];
- };
- results: {
- eyebrow: string;
- title: string;
- body: string;
- metrics: TitledDescription[];
- methodTitle: string;
- method: string[];
- };
- integrations: {
- eyebrow: string;
- title: string;
- body: string;
- items: string[];
- };
- security: {
- eyebrow: string;
- title: string;
- body: string;
- checks: TitledDescription[];
- };
- pricing: {
- eyebrow: string;
- title: string;
- body: string;
- factors: string[];
- action: HomepageAction;
- note: string;
- };
- faq: {
- eyebrow: string;
- title: string;
- items: HomepageFaqItem[];
- };
- final: {
- eyebrow: string;
- title: string;
- body: string;
- primary: HomepageAction;
- secondary: HomepageAction;
- };
-}
-
-const trustIcons: ApprovedIconName[] = ['calendar', 'car', 'check', 'settings'];
-const roleIcons: ApprovedIconName[] = ['settings', 'calendar', 'car', 'check'];
-const moduleIcons: ApprovedIconName[] = ['calendar', 'car', 'check', 'search', 'globe', 'settings'];
-const resultIcons: ApprovedIconName[] = ['calendar', 'settings', 'car', 'warning'];
-const securityIcons: ApprovedIconName[] = ['settings', 'search', 'globe', 'warning'];
-
-function pair(value: readonly string[], context: string): { title: string; description: string } {
- const title = value[0];
- const description = value[1];
- if (title === undefined || description === undefined) {
- throw new Error(`${context} must contain a title and description.`);
- }
- return { title, description };
-}
-
-function triple(
- value: readonly string[],
- context: string,
-): { ordinal: string; title: string; description: string } {
- const ordinal = value[0];
- const title = value[1];
- const description = value[2];
- if (ordinal === undefined || title === undefined || description === undefined) {
- throw new Error(`${context} must contain an ordinal, title, and description.`);
- }
- return { ordinal, title, description };
-}
-
-function mapPairs(
- values: readonly (readonly string[])[],
- prefix: string,
- icons: readonly ApprovedIconName[] = [],
-): TitledDescription[] {
- return values.map((value, index) => {
- const item = pair(value, `${prefix}[${index}]`);
- const icon = icons[index];
- return {
- id: `${prefix}-${index + 1}`,
- ...item,
- ...(icon ? { icon } : {}),
- };
- });
-}
-
-export function buildHomepageContent(
- homepage: HomepageMessages,
- shell: ShellMessages,
-): HomepageContent {
- const pendingReason = shell.states.pendingBody;
-
- return {
- hero: {
- eyebrow: homepage.hero.eyebrow,
- title: homepage.hero.title,
- body: homepage.hero.body,
- primary: { label: homepage.hero.primary, disabledReason: shell.header.demoUnavailable },
- secondary: { label: homepage.hero.secondary, disabledReason: pendingReason },
- preview: {
- title: homepage.preview.title,
- caption: homepage.hero.caption,
- illustrativeLabel: `${homepage.preview.today} · ${homepage.preview.branch}`,
- rows: [
- {
- id: 'preview-reservation-1',
- primary: `${homepage.preview.r1} · ${homepage.preview.taskAssign}`,
- secondary: homepage.preview.plate1,
- status: homepage.preview.statusReady,
- statusTone: 'success',
- },
- {
- id: 'preview-reservation-2',
- primary: `${homepage.preview.r2} · ${homepage.preview.taskInspect}`,
- secondary: homepage.preview.plate2,
- status: homepage.preview.statusDue,
- statusTone: 'warning',
- },
- ],
- },
- },
- trust: {
- eyebrow: homepage.trust.eyebrow,
- title: homepage.trust.title,
- body: homepage.trust.body,
- items: mapPairs(homepage.trust.items, 'lifecycle', trustIcons),
- },
- comparison: {
- eyebrow: homepage.comparison.eyebrow,
- title: homepage.comparison.title,
- body: homepage.comparison.body,
- beforeTitle: homepage.comparison.beforeTitle,
- beforeItems: homepage.comparison.beforeItems,
- afterTitle: homepage.comparison.afterTitle,
- afterItems: homepage.comparison.afterItems,
- },
- workflow: {
- eyebrow: homepage.workflow.eyebrow,
- title: homepage.workflow.title,
- body: homepage.workflow.body,
- steps: homepage.workflow.steps.map((value, index) => ({
- id: `workflow-${index + 1}`,
- ...triple(value, `workflow.steps[${index}]`),
- })),
- },
- roles: {
- eyebrow: homepage.roles.eyebrow,
- title: homepage.roles.title,
- items: mapPairs(homepage.roles.roles, 'role', roleIcons),
- },
- modules: {
- eyebrow: homepage.modules.eyebrow,
- title: homepage.modules.title,
- body: homepage.modules.body,
- items: mapPairs(homepage.modules.items, 'module', moduleIcons),
- },
- results: {
- eyebrow: homepage.results.eyebrow,
- title: homepage.results.title,
- body: homepage.results.body,
- metrics: mapPairs(homepage.results.metrics, 'measurement', resultIcons),
- methodTitle: homepage.results.methodTitle,
- method: homepage.results.method,
- },
- integrations: {
- eyebrow: homepage.integrations.eyebrow,
- title: homepage.integrations.title,
- body: homepage.integrations.body,
- items: homepage.integrations.items,
- },
- security: {
- eyebrow: homepage.security.eyebrow,
- title: homepage.security.title,
- body: homepage.security.body,
- checks: mapPairs(homepage.security.checks, 'security', securityIcons),
- },
- pricing: {
- eyebrow: homepage.pricing.eyebrow,
- title: homepage.pricing.title,
- body: homepage.pricing.body,
- factors: homepage.pricing.factors,
- action: { label: homepage.pricing.primary, disabledReason: pendingReason },
- note: homepage.pricing.note,
- },
- faq: {
- eyebrow: homepage.faq.eyebrow,
- title: homepage.faq.title,
- items: homepage.faq.items.map((value, index) => {
- const item = pair(value, `faq.items[${index}]`);
- return { id: `faq-${index + 1}`, title: item.title, body: item.description };
- }),
- },
- final: {
- eyebrow: homepage.final.eyebrow,
- title: homepage.final.title,
- body: homepage.final.body,
- primary: { label: homepage.final.primary, disabledReason: shell.header.demoUnavailable },
- secondary: { label: homepage.final.secondary, disabledReason: pendingReason },
- },
- };
-}
diff --git a/homepage/src/content/locales/ar/homepage.json b/homepage/src/content/locales/ar/homepage.json
deleted file mode 100644
index 3de56fc..0000000
--- a/homepage/src/content/locales/ar/homepage.json
+++ /dev/null
@@ -1,446 +0,0 @@
-{
- "meta": {
- "langName": "العربية",
- "dir": "rtl",
- "title": "RentalDriveGo | تشغيل مترابط لتأجير السيارات",
- "description": "أدِر الحجوزات والمركبات والعقود والتسليم والإرجاع ضمن منصة تشغيل مترابطة."
- },
- "header": {
- "nav": [
- [
- "product",
- "المنتج"
- ],
- [
- "workflow",
- "سير العمل"
- ],
- [
- "modules",
- "الوحدات"
- ],
- [
- "pricing",
- "التسعير"
- ],
- [
- "faq",
- "الأسئلة الشائعة"
- ]
- ],
- "login": "تسجيل الدخول",
- "demo": "أنشئ حسابًا",
- "menu": "فتح القائمة",
- "language": "اللغة",
- "theme": "المظهر",
- "light": "فاتح",
- "dark": "داكن",
- "system": "النظام"
- },
- "hero": {
- "eyebrow": "تشغيل تأجير السيارات ضمن نظام مترابط",
- "title": "أدِر كل عملية تأجير من الحجز حتى إعادة المركبة في نظام واحد.",
- "body": "اجمع الحجوزات والمركبات والعقود والتسليم والإرجاع في سجل تشغيلي واحد، كي يرى كل فريق ما يتطلب المتابعة بعد ذلك.",
- "primary": "أنشئ حسابًا للمنتج",
- "secondary": "شاهد جولة المنتج",
- "caption": "واجهة توضيحية للمنتج. يجب أن تستخدم الشاشات النهائية بيانات ولقطات معتمدة."
- },
- "trust": {
- "eyebrow": "تغطية تشغيلية",
- "title": "سير عمل واحد يغطي دورة التأجير كاملة.",
- "body": "حافظ على ترابط سياق العميل والمركبة والعقد والمهام مع تقدم عملية التأجير.",
- "items": [
- [
- "الحجز",
- "سجّل الطلب والبيانات المطلوبة."
- ],
- [
- "المركبة",
- "تابع التوافر والجاهزية."
- ],
- [
- "العقد",
- "اجمع مهام التسليم والسجلات في موضع واحد."
- ],
- [
- "الإرجاع",
- "سجّل الحالة وأغلق أعمال المتابعة."
- ]
- ]
- },
- "comparison": {
- "eyebrow": "لماذا التغيير",
- "title": "استبدل عمليات التسليم المجزأة بسير تشغيلي مشترك.",
- "body": "تكلفة الأدوات المنفصلة لا تظهر دائمًا في عطل كبير، بل تتراكم يوميًا عبر تكرار الإدخال وضياع السياق وتأخر المتابعة.",
- "beforeTitle": "تشغيل مجزأ",
- "beforeItems": [
- "نسخ بيانات الحجز بين أدوات متعددة",
- "تأكيد حالة المركبة عبر الرسائل",
- "متابعة مهام العقد بالاعتماد على الذاكرة",
- "اكتشاف مشكلات الإرجاع بعد مغادرة العميل"
- ],
- "afterTitle": "تشغيل مترابط",
- "afterItems": [
- "حالة حجز واحدة يراها الفريق",
- "ربط جاهزية المركبة بعملية التأجير",
- "إظهار متطلبات التسليم قبل تسليم المركبة",
- "إنشاء متابعة الإرجاع بينما لا يزال السياق واضحًا"
- ]
- },
- "workflow": {
- "eyebrow": "سير عمل متكامل",
- "title": "حرّك عملية التأجير إلى الأمام من دون فقدان السياق.",
- "body": "توضح كل خطوة القرار والمسؤول والإجراء التالي بدل دفن التشغيل داخل قائمة عامة من الميزات.",
- "steps": [
- [
- "01",
- "تسجيل الحجز",
- "سجّل التواريخ وبيانات السائق والمتطلبات والمصدر."
- ],
- [
- "02",
- "تخصيص مركبة جاهزة",
- "طابق التوافر مع الفئة والموقع وحالة الجاهزية."
- ],
- [
- "03",
- "إكمال التسليم",
- "تحقق من المستندات والفحص والعقد ومهام تسليم المركبة."
- ],
- [
- "04",
- "معالجة الإرجاع",
- "سجّل الحالة وأغلق الأعمال المفتوحة وأعد المركبة إلى التوافر."
- ]
- ]
- },
- "roles": {
- "eyebrow": "مصمم لفريق التشغيل",
- "title": "امنح كل دور السياق الذي يحتاجه من دون إنشاء نسخ متعددة من الحقيقة.",
- "roles": [
- [
- "المالك أو المدير",
- "اطّلع على عبء العمل والاستثناءات والمؤشرات التشغيلية عبر النشاط."
- ],
- [
- "فريق الحجوزات",
- "حوّل الطلبات إلى حجوزات مؤكدة مع عدد أقل من عمليات التسليم."
- ],
- [
- "الأسطول والتشغيل",
- "اعرف المركبات المتاحة أو المحجوبة أو المستحقة أو التي تنتظر إجراءً."
- ],
- [
- "مكتب الاستقبال",
- "حضّر مهام التسليم والإرجاع قبل انتظار العميل."
- ]
- ]
- },
- "modules": {
- "eyebrow": "وحدات المنتج",
- "title": "نواة مترابطة منظّمة حول أعمال التأجير الفعلية.",
- "body": "توضح حدود الوحدات المسؤوليات، بينما تمنع السجلات المشتركة فوضى النسخ واللصق المعتادة.",
- "items": [
- [
- "الحجوزات",
- "التوافر والتواريخ ومتطلبات العميل والحالة ضمن سير واحد."
- ],
- [
- "جاهزية الأسطول",
- "توافر المركبات والحجوزات التشغيلية وحالة الخدمة وسياق الموقع."
- ],
- [
- "عقود الإيجار",
- "متطلبات التسليم وحالة العقد والسجلات التشغيلية."
- ],
- [
- "فحص المركبة",
- "فحوص منظمة عند التسليم والإرجاع مع مهام متابعة."
- ],
- [
- "سجلات العملاء",
- "سجل التأجير وبيانات الاتصال المرتبطة بالعمل الجاري."
- ],
- [
- "التقارير التشغيلية",
- "تعريفات وعروض تشغيلية يمكن التحقق منها مقابل النشاط المصدر."
- ]
- ]
- },
- "results": {
- "eyebrow": "نتائج قابلة للقياس",
- "title": "قِس التغيير، لا الخطاب التسويقي.",
- "body": "لا ينبغي نشر أي نتيجة قبل اعتماد خط أساس حقيقي لعميل وفترة مقارنة وتعريف واضح للبيانات. لذلك تعرض هذه النسخة منهجية القياس بدل نسب مئوية مختلقة.",
- "metrics": [
- [
- "زمن معالجة الحجز",
- "من استلام طلب صالح للعمل حتى تأكيد الحجز."
- ],
- [
- "زمن إعادة المركبة إلى الجاهزية",
- "من تسجيل الإرجاع حتى حالة جاهز للتأجير."
- ],
- [
- "معدل استخدام الأسطول",
- "مقارنة أيام التوافر بأيام التأجير وفق تعريف واحد معتمد."
- ],
- [
- "متابعة حالات التأخير",
- "عمليات التأجير المتأخرة المفتوحة مع مسؤول وإجراء تالٍ."
- ]
- ],
- "methodTitle": "المنهجية المطلوبة قبل نشر أي نتيجة",
- "method": [
- "سجّل خط أساس قبل الإطلاق باستخدام تعريفات المؤشرات نفسها.",
- "قارن مواقع وفئات أسطول وفترات تشغيل متكافئة.",
- "انشر حجم العينة والاستثناءات وفترة القياس.",
- "اربط كل نتيجة منشورة بسجل دليل معتمد."
- ]
- },
- "integrations": {
- "eyebrow": "التكاملات",
- "title": "اربط الأنظمة التي يعتمد عليها تشغيلك حاليًا.",
- "body": "يُعتمد نطاق التكامل خلال مرحلة الاستكشاف ليتوافق مع سوقك وبنيتك الحالية وملكية البيانات. لا تُعرض أسماء شركاء قبل التحقق من توافر الموصل.",
- "items": [
- "المحاسبة والمالية",
- "المدفوعات ومبالغ الضمان",
- "الاتصال بالمركبات وبياناتها",
- "خدمات الهوية والمستندات",
- "البريد الإلكتروني والرسائل والإشعارات",
- "الموقع الإلكتروني وقنوات الحجز"
- ]
- },
- "security": {
- "eyebrow": "الأمن والموثوقية",
- "title": "قيّم المنصة بالأدلة، لا بالشعارات.",
- "body": "لا تُنشر شهادات أو وعود بمستوى الخدمة قبل وجود وثائق معتمدة. أثناء التقييم، اطلب إجابات محددة حول الضوابط المهمة لنشاطك.",
- "checks": [
- [
- "التحكم في الوصول",
- "الأدوار والصلاحيات وإجراءات المسؤول وإنهاء وصول المستخدم."
- ],
- [
- "قابلية التدقيق",
- "التغييرات المسجلة والقابلة للبحث والتصدير."
- ],
- [
- "معالجة البيانات",
- "الملكية والاحتفاظ والتصدير والحذف والجهات الفرعية."
- ],
- [
- "استمرارية العمل",
- "النسخ الاحتياطي والاستعادة والتواصل عند الحوادث والدعم."
- ]
- ]
- },
- "pricing": {
- "eyebrow": "معاينة التسعير",
- "title": "يجب أن يعكس التسعير طبيعة التشغيل الفعلية.",
- "body": "لا يُعرض سعر عام قبل اعتماد النموذج التجاري والحدود المشمولة. يبدأ التقييم بالعوامل التي تؤثر فعلًا في النطاق.",
- "factors": [
- "حجم الأسطول وعدد المواقع النشطة",
- "الوحدات المطلوبة وأدوار المستخدمين",
- "نطاق ترحيل البيانات والتهيئة",
- "متطلبات التكامل والدعم"
- ],
- "primary": "اطلب عرض سعر",
- "note": "لا تنشر هذه النسخة أسعار بطاقات أو خصومات أو حدود باقات."
- },
- "faq": {
- "eyebrow": "الأسئلة الشائعة",
- "title": "أسئلة ينبغي للمشتري طرحها قبل إنشاء الحساب.",
- "items": [
- [
- "هل RentalDriveGo مخصص فقط لشركات التأجير الكبيرة؟",
- "لا يزال السوق المستهدف وهيكل الباقات قيد الاعتماد. يجب أن يحدد إنشاء الحساب حجم الأسطول والمواقع وسير العمل والوحدات المطلوبة بدل افتراض أن إعدادًا واحدًا يناسب الجميع."
- ],
- [
- "هل يمكننا الاحتفاظ بأنظمتنا الحالية؟",
- "تُراجع التكاملات المحتملة خلال الاستكشاف. يجب تأكيد توافر الموصل وملكية البيانات وجهد الترحيل والمزامنة المستمرة قبل تقديم أي وعد."
- ],
- [
- "هل تدعم المنصة لغات متعددة؟",
- "يدعم الموقع ونظام التصميم المعتمد الإنجليزية والفرنسية والعربية. تستخدم العربية اتجاهًا حقيقيًا للمستند من اليمين إلى اليسار، لا مجرد محاذاة شكلية."
- ],
- [
- "كيف يُحسب السعر؟",
- "النموذج التجاري قيد الاعتماد. حتى ذلك الحين، يجب أن يوضح كل عرض النطاق والوحدات والحدود والتهيئة والتكاملات والدعم."
- ],
- [
- "ما الشهادات الأمنية المتوفرة؟",
- "لا تدّعي هذه النسخة وجود أي شهادة. يجب تقديم الأدلة الأمنية عبر وثائق معتمدة أثناء التقييم وعدم نشرها إلا بعد التحقق."
- ],
- [
- "ماذا يحدث أثناء إنشاء الحساب؟",
- "ينبغي أن ترسم الجلسة سير عملك من الحجز حتى الإرجاع، وتعرض المسار المناسب في المنتج، وتحدد احتياجات التكامل، وتوثق أسئلة الملاءمة غير المحسومة."
- ]
- ]
- },
- "final": {
- "eyebrow": "شاهد سير العمل كاملًا",
- "title": "أحضر عملية تأجير حقيقية.",
- "body": "سنتتبع عمليات التسليم والاستثناءات والمعلومات التي يحتاجها فريقك من الحجز حتى إعادة المركبة.",
- "primary": "أنشئ حسابًا للمنتج",
- "secondary": "تواصل مع المبيعات"
- },
- "footer": {
- "tagline": "تشغيل مترابط لتأجير السيارات من الحجز حتى الإرجاع.",
- "groups": [
- [
- "المنتج",
- [
- [
- "سير العمل",
- "#workflow"
- ],
- [
- "الوحدات",
- "#modules"
- ],
- [
- "التسعير",
- "#pricing"
- ]
- ]
- ],
- [
- "الشركة",
- [
- [
- "تواصل مع المبيعات",
- "#contact"
- ],
- [
- "أنشئ حسابًا",
- "#signup"
- ]
- ]
- ],
- [
- "الحساب",
- [
- [
- "تسجيل الدخول",
- "#login"
- ],
- [
- "اللغة",
- "#language"
- ],
- [
- "المظهر",
- "#theme"
- ]
- ]
- ],
- [
- "قانوني",
- [
- [
- "الخصوصية",
- "#!"
- ],
- [
- "الشروط",
- "#!"
- ],
- [
- "إمكانية الوصول",
- "#!"
- ]
- ]
- ]
- ],
- "copyright": "© 2026 RentalDriveGo. تظل الأدلة والادعاءات التجارية خاضعة للاعتماد."
- },
- "form": {
- "title": "أنشئ حسابًا للمنتج",
- "body": "أخبرنا كيف يعمل نشاط تأجير السيارات لديك. الحقول المطلوبة مميزة.",
- "name": "الاسم الكامل",
- "email": "البريد الإلكتروني للعمل",
- "company": "الشركة",
- "fleet": "الحجم التقريبي للأسطول",
- "market": "الدولة أو سوق التشغيل",
- "language": "اللغة المفضلة",
- "message": "ما الموضوع الذي ينبغي أن نركز عليه؟",
- "submit": "إنشاء حسابي",
- "privacy": "يتحقق وضع غير الإنتاج من سير العمل ثم يتخلص من الطلب. لا يُرسل أي عميل محتمل إلى نظام مبيعات. يظل نص الخصوصية والموافقة النهائي محظورًا إلى حين الاعتماد القانوني.",
- "close": "إغلاق",
- "required": "مطلوب",
- "emailError": "أدخل عنوان بريد إلكتروني صالحًا للعمل.",
- "requiredError": "هذا الحقل مطلوب.",
- "fleetOptions": [
- "اختر خيارًا",
- "من 1 إلى 9 مركبات",
- "من 10 إلى 24 مركبة",
- "من 25 إلى 49 مركبة",
- "من 50 إلى 99 مركبة",
- "من 100 إلى 249 مركبة",
- "250 مركبة أو أكثر"
- ],
- "langOptions": [
- "English",
- "Français",
- "العربية"
- ],
- "successTitle": "تم تسجيل إنشاء الحساب",
- "successBody": "اكتمل مسار الحجز في هذا النموذج الأولي. لم يُرسل أي طلب إلى نظام مبيعات فعلي.",
- "submitAnother": "إرسال طلب آخر",
- "submissionError": "تعذر على النموذج الأولي إكمال الطلب. ما زالت بياناتك متاحة. حاول مرة أخرى.",
- "errorSummary": "راجع الحقول المميزة ثم حاول مرة أخرى.",
- "optional": "اختياري",
- "messageHelp": "لا تُدرج معلومات شخصية حساسة أو معلومات دفع.",
- "messageCount": "حرفًا",
- "submitting": "جارٍ إرسال الطلب",
- "retry": "حاول مرة أخرى",
- "closeBlocked": "انتظر حتى يكتمل الإرسال الحالي قبل الإغلاق.",
- "invalidOptionError": "اختر خيارًا صالحًا.",
- "tooLongError": "هذا الإدخال طويل جدًا.",
- "configurationError": "إنشاء الحساب غير متاح لأن اعتماد تكامل الإنتاج لم يكتمل.",
- "timeoutError": "انتهت مهلة الطلب. لا تزال بياناتك متاحة؛ حاول مرة أخرى.",
- "duplicateError": "تمت معالجة هذا الطلب بالفعل. لم يُنشأ عميل محتمل ثانٍ.",
- "rateLimitError": "تم استلام عدد كبير جدًا من الطلبات. حاول مرة أخرى لاحقًا.",
- "genericError": "تعذر إكمال الطلب. لا تزال بياناتك متاحة.",
- "localModeLabel": "موصل محلي آمن",
- "marketDefault": "المغرب"
- },
- "preview": {
- "today": "اليوم",
- "branch": "الفرع المركزي",
- "title": "نظرة عامة على التشغيل",
- "reservations": "الحجوزات",
- "ready": "مركبات جاهزة",
- "attention": "تحتاج متابعة",
- "active": "عمليات تأجير نشطة",
- "r1": "تسليم الساعة 10:30",
- "r2": "إرجاع مستحق الساعة 14:00",
- "r3": "الفحص مطلوب",
- "vehicle": "المركبة",
- "customer": "العميل",
- "status": "الحالة",
- "task": "الإجراء التالي",
- "plate1": "ABC-2048",
- "plate2": "RNT-7712",
- "statusReady": "جاهزة",
- "statusDue": "مستحقة اليوم",
- "taskAssign": "تأكيد مستندات السائق",
- "taskInspect": "إكمال فحص الإرجاع"
- },
- "tour": {
- "title": "جولة في المنتج",
- "intro": "تابع عملية تأجير واحدة من الحجز حتى الإرجاع.",
- "back": "السابق",
- "next": "التالي",
- "finish": "إنهاء الجولة",
- "close": "إغلاق جولة المنتج",
- "step": "الخطوة",
- "of": "من"
- },
- "research": {
- "title": "وضع البحث",
- "events": "أحداث",
- "export": "تصدير الأحداث",
- "clear": "مسح",
- "close": "إغلاق"
- }
-}
diff --git a/homepage/src/content/locales/ar/shell.json b/homepage/src/content/locales/ar/shell.json
deleted file mode 100644
index 275b99d..0000000
--- a/homepage/src/content/locales/ar/shell.json
+++ /dev/null
@@ -1,94 +0,0 @@
-{
- "skipToContent": "الانتقال إلى المحتوى الرئيسي",
- "brandLabel": "الصفحة الرئيسية لـ RentalDriveGo",
- "foundationLabel": "أساس التطبيق",
- "header": {
- "navigationLabel": "التنقل الرئيسي",
- "controlsLabel": "عناصر التحكم في العرض واللغة",
- "menuOpen": "فتح قائمة التنقل",
- "menuClose": "إغلاق قائمة التنقل",
- "nav": {
- "product": "المنتج",
- "workflow": "سير العمل",
- "modules": "الوحدات",
- "pricing": "التسعير",
- "faq": "الأسئلة الشائعة"
- },
- "login": "تسجيل الدخول",
- "loginUnavailable": "وجهة تسجيل الدخول في انتظار الاعتماد",
- "demo": "أنشئ حسابًا",
- "demoUnavailable": "إنشاء الحساب في انتظار اعتماد التكامل"
- },
- "controls": {
- "language": "اللغة",
- "theme": "المظهر",
- "themeStatus": "تفضيل المظهر الحالي: {value}",
- "localeNames": {
- "en": "الإنجليزية",
- "fr": "الفرنسية",
- "ar": "العربية"
- },
- "themes": {
- "light": "فاتح",
- "dark": "داكن",
- "system": "النظام"
- }
- },
- "footer": {
- "landmarkLabel": "تذييل الموقع",
- "tagline": "تشغيل مترابط لتأجير السيارات من الحجز حتى الإرجاع.",
- "groups": {
- "product": "المنتج",
- "company": "الشركة",
- "account": "الحساب",
- "legal": "قانوني"
- },
- "links": {
- "workflow": "سير العمل",
- "modules": "الوحدات",
- "pricing": "التسعير",
- "faq": "الأسئلة الشائعة",
- "contact": "تواصل مع المبيعات",
- "demo": "أنشئ حسابًا",
- "login": "تسجيل الدخول",
- "privacy": "الخصوصية",
- "terms": "الشروط",
- "accessibility": "إمكانية الوصول"
- },
- "pending": "في انتظار الاعتماد",
- "copyrightPrefix": "©",
- "releaseNotice": "يظل النشر العام متوقفًا حتى معالجة العوائق الموثقة."
- },
- "states": {
- "loadingTitle": "جارٍ التحميل",
- "loadingBody": "جارٍ تجهيز هيكل التطبيق.",
- "errorTitle": "حدث خطأ",
- "errorBody": "تعذر عرض الصفحة، ولم تُكشف أي تفاصيل تقنية.",
- "retry": "إعادة المحاولة",
- "notFoundTitle": "الصفحة غير موجودة",
- "notFoundBody": "لا يطابق هذا العنوان مسارًا محليًا معتمدًا.",
- "backHome": "العودة إلى الصفحة الرئيسية",
- "pendingTitle": "الوجهة غير متاحة",
- "pendingBody": "هذا المسار معرّف، لكن محتواه أو وجهته الإنتاجية ما زالت في انتظار الاعتماد."
- },
- "foundation": {
- "eyebrow": "أساس المرحلة 11",
- "title": "هيكل تطبيق محلي ومتوافق مع المظاهر",
- "body": "تثبت هذه النسخة الأسس المرئية وأسس التوجيه والاتجاه وإمكانية الوصول والبيانات الوصفية من دون الادعاء بأن الصفحة التسويقية الكاملة أصبحت جاهزة.",
- "readinessLabel": "مجالات الأساس المنفذة",
- "items": [
- ["الرموز الدلالية", "تستهلك أنماط المكونات طبقة دلالية موثقة."],
- ["سلوك المظهر", "تُحل تفضيلات المظهر الفاتح والداكن والنظام وتُحفظ."],
- ["التوطين", "تُعرض الإنجليزية والفرنسية والعربية من مسارات مسبوقة باللغة."],
- ["اتجاه المستند", "تُعرض العربية باتجاه حقيقي من اليمين إلى اليسار على مستوى المستند."],
- ["إمكانية الوصول", "رابط التخطي والتركيز المرئي والمعالم وتقليل الحركة مدمجة."],
- ["هيكل التطبيق", "يتوفر رأس متجاوب وتنقل للجوال وتذييل وحالات عامة."]
- ],
- "deferredLabel": "وجهات الصفحة الرئيسية المؤجلة",
- "deferredBody": "ترتبط وجهة التنقل المعتمدة هذه ببيان المسارات، بينما يظل قسمها الإنتاجي خارج نطاق المرحلة 11."
- },
- "metadata": {
- "pendingTitle": "وجهة غير متاحة | RentalDriveGo",
- "pendingDescription": "وجهة RentalDriveGo المحلية هذه معرّفة، لكن نشرها في بيئة الإنتاج غير معتمد."
- }
-}
diff --git a/homepage/src/content/locales/en/homepage.json b/homepage/src/content/locales/en/homepage.json
deleted file mode 100644
index a256abe..0000000
--- a/homepage/src/content/locales/en/homepage.json
+++ /dev/null
@@ -1,324 +0,0 @@
-{
- "meta": {
- "langName": "English",
- "dir": "ltr",
- "title": "RentalDriveGo | Connected rental-car operations",
- "description": "Run reservations, vehicles, agreements, handovers, and returns in one connected rental operations platform."
- },
- "header": {
- "nav": [
- ["product", "Product"],
- ["workflow", "Workflow"],
- ["modules", "Modules"],
- ["pricing", "Pricing"],
- ["faq", "FAQ"]
- ],
- "login": "Log in",
- "demo": "Create account",
- "menu": "Open menu",
- "language": "Language",
- "theme": "Theme",
- "light": "Light",
- "dark": "Dark",
- "system": "System"
- },
- "hero": {
- "eyebrow": "Rental operations, connected",
- "title": "Run every rental from reservation to return in one system.",
- "body": "Give reservations, fleet, agreements, handovers, and returns one operational record, so each team sees what needs attention next.",
- "primary": "Create account",
- "secondary": "Watch product tour",
- "caption": "Illustrative product interface. Final screens must use approved product data and captures."
- },
- "trust": {
- "eyebrow": "Operational coverage",
- "title": "One workflow across the rental lifecycle.",
- "body": "Keep the customer, vehicle, agreement, and task context connected as the rental moves forward.",
- "items": [
- ["Reservation", "Capture the request and required details."],
- ["Vehicle", "Track availability and readiness."],
- ["Agreement", "Keep pickup tasks and records together."],
- ["Return", "Record condition and close follow-up work."]
- ]
- },
- "comparison": {
- "eyebrow": "Why change",
- "title": "Replace fragmented handoffs with a shared operating flow.",
- "body": "The cost of disconnected tools is rarely one dramatic failure. It is the daily accumulation of duplicate entry, missing context, and delayed follow-up.",
- "beforeTitle": "Fragmented operation",
- "beforeItems": [
- "Reservation details copied between tools",
- "Vehicle status confirmed through messages",
- "Agreement tasks tracked from memory",
- "Return issues discovered after the customer leaves"
- ],
- "afterTitle": "Connected operation",
- "afterItems": [
- "One reservation status visible to the team",
- "Vehicle readiness attached to the rental",
- "Pickup requirements shown before handover",
- "Return follow-up created while context is fresh"
- ]
- },
- "workflow": {
- "eyebrow": "End-to-end workflow",
- "title": "Move the rental forward without losing the thread.",
- "body": "Each step exposes the decision, owner, and next action instead of burying operations inside a generic feature list.",
- "steps": [
- ["01", "Capture the reservation", "Record dates, driver details, requirements, and source."],
- ["02", "Assign a ready vehicle", "Match availability with class, location, and readiness."],
- ["03", "Complete pickup", "Confirm documents, inspection, agreement, and handover tasks."],
- ["04", "Process the return", "Record condition, close open work, and release the vehicle."]
- ]
- },
- "roles": {
- "eyebrow": "Built for the operating team",
- "title": "Give each role the context it needs, without creating separate versions of the truth.",
- "roles": [
- ["Owner or manager", "See workload, exceptions, and operating signals across the business."],
- ["Reservations team", "Move requests from inquiry to confirmed rental with fewer handoffs."],
- [
- "Fleet and operations",
- "Know which vehicles are available, blocked, due, or awaiting action."
- ],
- ["Front desk", "Prepare pickup and return tasks before the customer is waiting."]
- ]
- },
- "modules": {
- "eyebrow": "Product modules",
- "title": "A connected core, organized around real rental work.",
- "body": "Module boundaries clarify responsibility while shared records prevent the usual copy-and-paste archaeology.",
- "items": [
- ["Reservations", "Availability, dates, customer requirements, and status in one flow."],
- ["Fleet readiness", "Vehicle availability, blocks, service state, and location context."],
- ["Rental agreements", "Pickup requirements, agreement status, and operational records."],
- [
- "Vehicle inspections",
- "Structured pickup and return condition checks with follow-up tasks."
- ],
- ["Customer records", "Rental history and contact context attached to active work."],
- [
- "Operations reporting",
- "Definitions and operational views that can be validated against source activity."
- ]
- ]
- },
- "results": {
- "eyebrow": "Measured results",
- "title": "Measure the change, not the marketing.",
- "body": "No outcome claim should appear until a real customer baseline, comparison period, and data definition are approved. The production page therefore shows the measurement plan instead of fictional percentages.",
- "metrics": [
- ["Reservation handling time", "From usable request to confirmed reservation."],
- ["Vehicle turnaround time", "From return recorded to ready-for-rental status."],
- [
- "Fleet utilization",
- "Available rental days compared with rented days, using one approved definition."
- ],
- ["Overdue follow-up", "Open overdue rentals with an assigned owner and next action."]
- ],
- "methodTitle": "Methodology required before publishing results",
- "method": [
- "Capture a pre-launch baseline using the same KPI definitions.",
- "Compare equivalent locations, fleet classes, and operating periods.",
- "Report sample size, exclusions, and measurement window.",
- "Link every public result to an approved evidence record."
- ]
- },
- "integrations": {
- "eyebrow": "Integrations",
- "title": "Connect the systems your operation already depends on.",
- "body": "Integration scope is confirmed during discovery so the proposal reflects your market, current stack, and data ownership. Named partners are not shown until connector availability is verified.",
- "items": [
- "Accounting and finance",
- "Payments and deposits",
- "Telematics and vehicle data",
- "Identity and document services",
- "Email, SMS, and notifications",
- "Website and booking channels"
- ]
- },
- "security": {
- "eyebrow": "Security and reliability",
- "title": "Evaluate the platform with evidence, not badges.",
- "body": "Certifications and service-level claims remain unpublished until approved documentation exists. During evaluation, request concrete answers for the controls that matter to your operation.",
- "checks": [
- ["Access control", "Roles, permissions, administrator actions, and offboarding."],
- ["Auditability", "Which changes are recorded, searchable, and exportable."],
- ["Data handling", "Ownership, retention, export, deletion, and subprocessors."],
- ["Continuity", "Backup, recovery, incident communication, and support process."]
- ]
- },
- "pricing": {
- "eyebrow": "Pricing preview",
- "title": "Pricing should match the operation you actually run.",
- "body": "A public price is not shown until the commercial model and included limits are approved. The evaluation starts with the factors that materially affect scope.",
- "factors": [
- "Fleet size and active locations",
- "Required modules and user roles",
- "Migration and onboarding scope",
- "Integration and support requirements"
- ],
- "primary": "Request pricing",
- "note": "No card prices, discounts, or package limits are published in this baseline."
- },
- "faq": {
- "eyebrow": "FAQ",
- "title": "Questions buyers should ask before creating an account.",
- "items": [
- [
- "Is RentalDriveGo only for large rental companies?",
- "The final market and packaging are still being defined. The onboarding should qualify fleet size, locations, workflows, and required modules rather than pretend one setup fits everyone."
- ],
- [
- "Can we keep our existing systems?",
- "Potential integrations are reviewed during discovery. Connector availability, data ownership, migration effort, and ongoing synchronization must be confirmed before they are promised."
- ],
- [
- "Does the platform support multiple languages?",
- "The website and approved design system support English, French, and Arabic. Arabic uses true document-level right-to-left behavior rather than a mirrored-looking patch."
- ],
- [
- "How is pricing calculated?",
- "The commercial model is pending approval. Until then, proposals should state the scope, included modules, limits, onboarding, integrations, and support explicitly."
- ],
- [
- "What security certifications do you have?",
- "No certification is claimed in this baseline. Security evidence should be supplied through approved documentation during evaluation and published only after verification."
- ],
- [
- "What happens during account creation?",
- "The session should map your current reservation-to-return flow, show the relevant product path, identify integration needs, and document unresolved fit questions."
- ]
- ]
- },
- "final": {
- "eyebrow": "See the full workflow",
- "title": "Bring one real rental process.",
- "body": "We will trace the handoffs, exceptions, and information your team needs from reservation through vehicle return.",
- "primary": "Create account",
- "secondary": "Contact sales"
- },
- "footer": {
- "tagline": "Connected rental-car operations from reservation to return.",
- "groups": [
- [
- "Product",
- [
- ["Workflow", "#workflow"],
- ["Modules", "#modules"],
- ["Pricing", "#pricing"]
- ]
- ],
- [
- "Company",
- [
- ["Contact sales", "#contact"],
- ["Create account", "#signup"]
- ]
- ],
- [
- "Account",
- [
- ["Log in", "#login"],
- ["Language", "#language"],
- ["Theme", "#theme"]
- ]
- ],
- [
- "Legal",
- [
- ["Privacy", "#!"],
- ["Terms", "#!"],
- ["Accessibility", "#!"]
- ]
- ]
- ],
- "copyright": "© 2026 RentalDriveGo. Evidence and commercial claims remain subject to approval."
- },
- "form": {
- "title": "Create account",
- "body": "Tell us how your rental operation works. Required fields are marked.",
- "name": "Full name",
- "email": "Work email",
- "company": "Company",
- "fleet": "Approximate fleet size",
- "market": "Country or operating market",
- "marketDefault": "Morocco",
- "language": "Preferred language",
- "message": "What should we focus on?",
- "submit": "Create my account",
- "privacy": "Non-production mode validates the workflow and discards the request. No lead is sent to a sales system. Final privacy and consent text remains blocked pending legal approval.",
- "close": "Close",
- "required": "Required",
- "emailError": "Enter a valid work email address.",
- "requiredError": "This field is required.",
- "fleetOptions": [
- "Select an option",
- "1–9 vehicles",
- "10–24 vehicles",
- "25–49 vehicles",
- "50–99 vehicles",
- "100–249 vehicles",
- "250 or more vehicles"
- ],
- "langOptions": ["English", "Français", "العربية"],
- "successTitle": "Account creation recorded",
- "successBody": "This prototype has completed the booking flow. No request was sent to a real sales system.",
- "submitAnother": "Submit another request",
- "submissionError": "The prototype could not complete the request. Your entries are still available. Try again.",
- "errorSummary": "Review the highlighted fields and try again.",
- "optional": "Optional",
- "messageHelp": "Do not include sensitive personal or payment information.",
- "messageCount": "characters",
- "submitting": "Submitting request",
- "retry": "Try again",
- "closeBlocked": "Wait for the current submission to finish before closing.",
- "invalidOptionError": "Select a valid option.",
- "tooLongError": "This entry is too long.",
- "configurationError": "Account creation is not available because production integration approval is incomplete.",
- "timeoutError": "The request timed out. Your entries are still available; try again.",
- "duplicateError": "This request has already been processed. No second lead was created.",
- "rateLimitError": "Too many requests were received. Try again later.",
- "genericError": "The request could not be completed. Your entries are still available.",
- "localModeLabel": "Safe local adapter"
- },
- "preview": {
- "today": "Today",
- "branch": "Central branch",
- "title": "Operations overview",
- "reservations": "Reservations",
- "ready": "Vehicles ready",
- "attention": "Needs attention",
- "active": "Active rentals",
- "r1": "Pickup at 10:30",
- "r2": "Return due at 14:00",
- "r3": "Inspection required",
- "vehicle": "Vehicle",
- "customer": "Customer",
- "status": "Status",
- "task": "Next action",
- "plate1": "ABC-2048",
- "plate2": "RNT-7712",
- "statusReady": "Ready",
- "statusDue": "Due today",
- "taskAssign": "Confirm driver documents",
- "taskInspect": "Complete return inspection"
- },
- "tour": {
- "title": "Product tour",
- "intro": "Follow one rental from reservation through return.",
- "back": "Previous",
- "next": "Next",
- "finish": "Finish tour",
- "close": "Close product tour",
- "step": "Step",
- "of": "of"
- },
- "research": {
- "title": "Research mode",
- "events": "events",
- "export": "Export events",
- "clear": "Clear",
- "close": "Close"
- }
-}
diff --git a/homepage/src/content/locales/en/shell.json b/homepage/src/content/locales/en/shell.json
deleted file mode 100644
index 7e6097c..0000000
--- a/homepage/src/content/locales/en/shell.json
+++ /dev/null
@@ -1,100 +0,0 @@
-{
- "skipToContent": "Skip to main content",
- "brandLabel": "RentalDriveGo home",
- "foundationLabel": "Application foundation",
- "header": {
- "navigationLabel": "Primary navigation",
- "controlsLabel": "Display and language controls",
- "menuOpen": "Open navigation menu",
- "menuClose": "Close navigation menu",
- "nav": {
- "product": "Product",
- "workflow": "Workflow",
- "modules": "Modules",
- "pricing": "Pricing",
- "faq": "FAQ"
- },
- "login": "Log in",
- "loginUnavailable": "Login destination pending approval",
- "demo": "Create account",
- "demoUnavailable": "Account creation pending integration approval"
- },
- "controls": {
- "language": "Language",
- "theme": "Theme",
- "themeStatus": "Current theme preference: {value}",
- "localeNames": {
- "en": "English",
- "fr": "French",
- "ar": "Arabic"
- },
- "themes": {
- "light": "Light",
- "dark": "Dark",
- "system": "System"
- }
- },
- "footer": {
- "landmarkLabel": "Site footer",
- "tagline": "Connected rental-car operations from reservation to return.",
- "groups": {
- "product": "Product",
- "company": "Company",
- "account": "Account",
- "legal": "Legal"
- },
- "links": {
- "workflow": "Workflow",
- "modules": "Modules",
- "pricing": "Pricing",
- "faq": "FAQ",
- "contact": "Contact sales",
- "demo": "Create account",
- "login": "Log in",
- "privacy": "Privacy",
- "terms": "Terms",
- "accessibility": "Accessibility"
- },
- "pending": "Pending approval",
- "copyrightPrefix": "©",
- "releaseNotice": "Public release remains gated until documented blockers are resolved."
- },
- "states": {
- "loadingTitle": "Loading",
- "loadingBody": "Preparing the application shell.",
- "errorTitle": "Something went wrong",
- "errorBody": "The page could not be displayed. Technical details were not exposed.",
- "retry": "Try again",
- "notFoundTitle": "Page not found",
- "notFoundBody": "This address does not match an approved localized route.",
- "backHome": "Return home",
- "pendingTitle": "Destination not available",
- "pendingBody": "This route is defined, but its production content or destination is still awaiting approval."
- },
- "foundation": {
- "eyebrow": "Phase 11 foundation",
- "title": "A localized, theme-aware application shell",
- "body": "This implementation proves the shared visual, routing, direction, accessibility, and metadata foundations without pretending the full marketing homepage already exists.",
- "readinessLabel": "Implemented foundation areas",
- "items": [
- ["Semantic tokens", "Component styles consume a documented semantic layer."],
- ["Theme behavior", "Light, dark, and system preferences resolve and persist."],
- ["Localization", "English, French, and Arabic render from locale-prefixed routes."],
- ["Directionality", "Arabic renders with document-level right-to-left direction."],
- [
- "Accessibility",
- "Skip navigation, focus treatment, landmarks, and reduced motion are built in."
- ],
- [
- "Application shell",
- "Responsive header, mobile navigation, footer, and global states are available."
- ]
- ],
- "deferredLabel": "Deferred homepage destinations",
- "deferredBody": "This approved navigation destination is wired to the route manifest, while its production section remains outside Phase 11."
- },
- "metadata": {
- "pendingTitle": "Unavailable destination | RentalDriveGo",
- "pendingDescription": "This localized RentalDriveGo destination is defined but not approved for production publication."
- }
-}
diff --git a/homepage/src/content/locales/fr/homepage.json b/homepage/src/content/locales/fr/homepage.json
deleted file mode 100644
index a9db26e..0000000
--- a/homepage/src/content/locales/fr/homepage.json
+++ /dev/null
@@ -1,446 +0,0 @@
-{
- "meta": {
- "langName": "Français",
- "dir": "ltr",
- "title": "RentalDriveGo | Opérations de location connectées",
- "description": "Pilotez les réservations, véhicules, contrats, remises et retours dans une plateforme connectée."
- },
- "header": {
- "nav": [
- [
- "product",
- "Produit"
- ],
- [
- "workflow",
- "Parcours"
- ],
- [
- "modules",
- "Modules"
- ],
- [
- "pricing",
- "Tarifs"
- ],
- [
- "faq",
- "FAQ"
- ]
- ],
- "login": "Se connecter",
- "demo": "Créer un compte",
- "menu": "Ouvrir le menu",
- "language": "Langue",
- "theme": "Thème",
- "light": "Clair",
- "dark": "Sombre",
- "system": "Système"
- },
- "hero": {
- "eyebrow": "Opérations de location connectées",
- "title": "Pilotez chaque location, de la réservation au retour du véhicule, dans un seul système.",
- "body": "Reliez les réservations, le parc, les contrats, les remises et les retours dans un même dossier opérationnel, afin que chaque équipe sache quelle action vient ensuite.",
- "primary": "Créer un compte",
- "secondary": "Voir la visite du produit",
- "caption": "Interface produit illustrative. Les écrans définitifs doivent utiliser des données et captures approuvées."
- },
- "trust": {
- "eyebrow": "Couverture opérationnelle",
- "title": "Un même parcours pour tout le cycle de location.",
- "body": "Conservez le contexte du client, du véhicule, du contrat et des tâches au fil de la location.",
- "items": [
- [
- "Réservation",
- "Saisir la demande et les informations nécessaires."
- ],
- [
- "Véhicule",
- "Suivre la disponibilité et l’état de préparation."
- ],
- [
- "Contrat",
- "Regrouper les tâches de départ et les documents."
- ],
- [
- "Retour",
- "Enregistrer l’état et clôturer les actions de suivi."
- ]
- ]
- },
- "comparison": {
- "eyebrow": "Pourquoi changer",
- "title": "Remplacez les transmissions fragmentées par un flux opérationnel partagé.",
- "body": "Le coût des outils déconnectés n’est pas toujours une panne spectaculaire. Il s’accumule dans les doubles saisies, le contexte perdu et les suivis tardifs.",
- "beforeTitle": "Fonctionnement fragmenté",
- "beforeItems": [
- "Informations de réservation recopiées entre plusieurs outils",
- "État du véhicule confirmé par messages",
- "Tâches contractuelles suivies de mémoire",
- "Problèmes de retour découverts après le départ du client"
- ],
- "afterTitle": "Fonctionnement connecté",
- "afterItems": [
- "Un statut de réservation visible par l’équipe",
- "Préparation du véhicule rattachée à la location",
- "Exigences de départ affichées avant la remise",
- "Suivi du retour créé pendant que le contexte est encore précis"
- ]
- },
- "workflow": {
- "eyebrow": "Parcours de bout en bout",
- "title": "Faites avancer la location sans perdre le fil.",
- "body": "Chaque étape met en évidence la décision, le responsable et l’action suivante, au lieu d’enfouir le travail dans une liste générique de fonctionnalités.",
- "steps": [
- [
- "01",
- "Saisir la réservation",
- "Enregistrer les dates, le conducteur, les exigences et la source."
- ],
- [
- "02",
- "Affecter un véhicule prêt",
- "Faire correspondre disponibilité, catégorie, agence et état de préparation."
- ],
- [
- "03",
- "Finaliser le départ",
- "Confirmer documents, inspection, contrat et tâches de remise."
- ],
- [
- "04",
- "Traiter le retour",
- "Enregistrer l’état, clôturer les actions et libérer le véhicule."
- ]
- ]
- },
- "roles": {
- "eyebrow": "Conçu pour l’équipe opérationnelle",
- "title": "Donnez à chaque rôle le contexte utile, sans multiplier les versions de la vérité.",
- "roles": [
- [
- "Direction ou responsable",
- "Voir la charge, les exceptions et les signaux d’exploitation."
- ],
- [
- "Équipe réservations",
- "Faire passer les demandes vers une location confirmée avec moins de transmissions."
- ],
- [
- "Parc et opérations",
- "Savoir quels véhicules sont disponibles, bloqués, attendus ou en attente d’action."
- ],
- [
- "Comptoir",
- "Préparer les tâches de départ et de retour avant que le client n’attende."
- ]
- ]
- },
- "modules": {
- "eyebrow": "Modules produit",
- "title": "Un socle connecté, organisé autour du travail réel de location.",
- "body": "Les modules clarifient les responsabilités, tandis que les dossiers partagés évitent l’archéologie du copier-coller.",
- "items": [
- [
- "Réservations",
- "Disponibilités, dates, besoins du client et statut dans un même flux."
- ],
- [
- "Préparation du parc",
- "Disponibilité, blocages, état de service et contexte d’agence."
- ],
- [
- "Contrats de location",
- "Exigences de départ, état du contrat et dossiers opérationnels."
- ],
- [
- "Inspections des véhicules",
- "Contrôles structurés au départ et au retour avec actions de suivi."
- ],
- [
- "Dossiers clients",
- "Historique de location et coordonnées rattachés au travail en cours."
- ],
- [
- "Rapports opérationnels",
- "Définitions et vues vérifiables par rapport à l’activité source."
- ]
- ]
- },
- "results": {
- "eyebrow": "Résultats mesurés",
- "title": "Mesurez le changement, pas le discours commercial.",
- "body": "Aucun résultat ne doit être publié sans référence client, période de comparaison et définition de données approuvées. Cette version présente donc la méthode plutôt que des pourcentages fictifs.",
- "metrics": [
- [
- "Temps de traitement des réservations",
- "De la demande exploitable à la réservation confirmée."
- ],
- [
- "Temps de remise en service",
- "Du retour enregistré au statut prêt à louer."
- ],
- [
- "Taux d’utilisation du parc",
- "Jours louables comparés aux jours loués, selon une définition approuvée."
- ],
- [
- "Suivi des retards",
- "Locations en retard avec responsable et prochaine action."
- ]
- ],
- "methodTitle": "Méthode requise avant toute publication",
- "method": [
- "Établir une référence avant lancement avec les mêmes définitions de KPI.",
- "Comparer des agences, catégories de parc et périodes équivalentes.",
- "Publier la taille de l’échantillon, les exclusions et la fenêtre de mesure.",
- "Relier chaque résultat public à une preuve approuvée."
- ]
- },
- "integrations": {
- "eyebrow": "Intégrations",
- "title": "Reliez les systèmes dont votre exploitation dépend déjà.",
- "body": "Le périmètre d’intégration est confirmé pendant la découverte afin de refléter votre marché, votre environnement actuel et la propriété des données. Aucun partenaire n’est nommé avant vérification du connecteur.",
- "items": [
- "Comptabilité et finance",
- "Paiements et dépôts",
- "Télématique et données véhicules",
- "Identité et documents",
- "E-mail, SMS et notifications",
- "Site web et canaux de réservation"
- ]
- },
- "security": {
- "eyebrow": "Sécurité et fiabilité",
- "title": "Évaluez la plateforme avec des preuves, pas avec des badges.",
- "body": "Aucune certification ni promesse de niveau de service n’est publiée sans documentation approuvée. Pendant l’évaluation, demandez des réponses concrètes sur les contrôles utiles à votre activité.",
- "checks": [
- [
- "Contrôle des accès",
- "Rôles, autorisations, actions administrateur et départs."
- ],
- [
- "Traçabilité",
- "Modifications enregistrées, recherchables et exportables."
- ],
- [
- "Gestion des données",
- "Propriété, conservation, export, suppression et sous-traitants."
- ],
- [
- "Continuité",
- "Sauvegarde, reprise, communication d’incident et support."
- ]
- ]
- },
- "pricing": {
- "eyebrow": "Aperçu des tarifs",
- "title": "Le tarif doit correspondre à l’exploitation que vous gérez réellement.",
- "body": "Aucun prix public n’est affiché tant que le modèle commercial et les limites incluses ne sont pas approuvés. L’évaluation commence par les facteurs qui influencent réellement le périmètre.",
- "factors": [
- "Taille du parc et agences actives",
- "Modules requis et rôles utilisateurs",
- "Migration et accompagnement au démarrage",
- "Intégrations et niveau de support"
- ],
- "primary": "Demander un tarif",
- "note": "Aucun prix, remise ou plafond de forfait n’est publié dans cette version."
- },
- "faq": {
- "eyebrow": "FAQ",
- "title": "Les questions à poser avant de créer un compte.",
- "items": [
- [
- "RentalDriveGo est-il réservé aux grands loueurs ?",
- "Le marché cible et les offres finales restent à définir. La création de compte doit qualifier la taille du parc, les agences, les flux et les modules nécessaires au lieu de prétendre qu’une configuration convient à tous."
- ],
- [
- "Pouvons-nous conserver nos systèmes actuels ?",
- "Les intégrations possibles sont examinées pendant la découverte. La disponibilité du connecteur, la propriété des données, l’effort de migration et la synchronisation doivent être confirmés avant toute promesse."
- ],
- [
- "La plateforme prend-elle en charge plusieurs langues ?",
- "Le site et le système de design approuvé prennent en charge l’anglais, le français et l’arabe. L’arabe utilise une vraie direction de document de droite à gauche."
- ],
- [
- "Comment les tarifs sont-ils calculés ?",
- "Le modèle commercial est en attente d’approbation. En attendant, toute proposition doit préciser le périmètre, les modules, les limites, l’accompagnement, les intégrations et le support."
- ],
- [
- "Quelles certifications de sécurité possédez-vous ?",
- "Aucune certification n’est revendiquée dans cette version. Les preuves de sécurité doivent être fournies pendant l’évaluation puis publiées uniquement après vérification."
- ],
- [
- "Que se passe-t-il pendant la création du compte ?",
- "La session doit cartographier votre flux de la réservation au retour, montrer le parcours produit pertinent, identifier les intégrations et consigner les questions de compatibilité non résolues."
- ]
- ]
- },
- "final": {
- "eyebrow": "Voir le parcours complet",
- "title": "Apportez un processus de location réel ",
- "body": "Nous suivrons les transmissions, les exceptions et les informations dont votre équipe a besoin de la réservation au retour du véhicule.",
- "primary": "Créer un compte",
- "secondary": "Contacter le service commercial"
- },
- "footer": {
- "tagline": "Des opérations de location connectées, de la réservation au retour.",
- "groups": [
- [
- "Produit",
- [
- [
- "Parcours",
- "#workflow"
- ],
- [
- "Modules",
- "#modules"
- ],
- [
- "Tarifs",
- "#pricing"
- ]
- ]
- ],
- [
- "Entreprise",
- [
- [
- "Contacter le service commercial",
- "#contact"
- ],
- [
- "Créer un compte",
- "#signup"
- ]
- ]
- ],
- [
- "Compte",
- [
- [
- "Se connecter",
- "#login"
- ],
- [
- "Langue",
- "#language"
- ],
- [
- "Thème",
- "#theme"
- ]
- ]
- ],
- [
- "Mentions légales",
- [
- [
- "Confidentialité",
- "#!"
- ],
- [
- "Conditions",
- "#!"
- ],
- [
- "Accessibilité",
- "#!"
- ]
- ]
- ]
- ],
- "copyright": "© 2026 RentalDriveGo. Les preuves et conditions commerciales restent soumises à approbation."
- },
- "form": {
- "title": "Créer un compte du produit",
- "body": "Décrivez-nous le fonctionnement de votre activité de location. Les champs obligatoires sont signalés.",
- "name": "Nom complet",
- "email": "Adresse e-mail professionnelle",
- "company": "Entreprise",
- "fleet": "Taille approximative du parc",
- "market": "Pays ou marché d’exploitation",
- "language": "Langue souhaitée",
- "message": "Sur quoi devons-nous nous concentrer ?",
- "submit": "Créer mon compte",
- "privacy": "Le mode hors production valide le parcours puis supprime la demande. Aucun prospect n’est envoyé à un système commercial. Le texte final de confidentialité et de consentement reste bloqué dans l’attente de l’approbation juridique.",
- "close": "Fermer",
- "required": "Obligatoire",
- "emailError": "Saisissez une adresse e-mail professionnelle valide.",
- "requiredError": "Ce champ est obligatoire.",
- "fleetOptions": [
- "Sélectionnez une option",
- "1 à 9 véhicules",
- "10 à 24 véhicules",
- "25 à 49 véhicules",
- "50 à 99 véhicules",
- "100 à 249 véhicules",
- "250 véhicules ou plus"
- ],
- "langOptions": [
- "English",
- "Français",
- "العربية"
- ],
- "successTitle": "Création de compte enregistrée",
- "successBody": "Ce prototype a terminé le parcours de réservation. Aucune demande n’a été envoyée à un véritable système commercial.",
- "submitAnother": "Envoyer une autre demande",
- "submissionError": "Le prototype n’a pas pu terminer la demande. Vos informations sont conservées. Réessayez.",
- "errorSummary": "Vérifiez les champs signalés, puis réessayez.",
- "optional": "Facultatif",
- "messageHelp": "N’incluez aucune donnée personnelle sensible ni information de paiement.",
- "messageCount": "caractères",
- "submitting": "Envoi de la demande",
- "retry": "Réessayer",
- "closeBlocked": "Attendez la fin de l’envoi en cours avant de fermer.",
- "invalidOptionError": "Sélectionnez une option valide.",
- "tooLongError": "Cette saisie est trop longue.",
- "configurationError": "L’envoi de création de compte est indisponible car l’intégration de production n’est pas encore approuvée.",
- "timeoutError": "La demande a expiré. Vos informations sont toujours disponibles ; réessayez.",
- "duplicateError": "Cette demande a déjà été traitée. Aucun deuxième prospect n’a été créé.",
- "rateLimitError": "Trop de demandes ont été reçues. Réessayez plus tard.",
- "genericError": "La demande n’a pas pu être terminée. Vos informations sont toujours disponibles.",
- "localModeLabel": "Adaptateur local sécurisé",
- "marketDefault": "Maroc"
- },
- "preview": {
- "today": "Aujourd’hui",
- "branch": "Agence centrale",
- "title": "Vue des opérations",
- "reservations": "Réservations",
- "ready": "Véhicules prêts",
- "attention": "À traiter",
- "active": "Locations actives",
- "r1": "Départ à 10 h 30",
- "r2": "Retour prévu à 14 h 00",
- "r3": "Inspection requise",
- "vehicle": "Véhicule",
- "customer": "Client",
- "status": "Statut",
- "task": "Prochaine action",
- "plate1": "ABC-2048",
- "plate2": "RNT-7712",
- "statusReady": "Prêt",
- "statusDue": "Prévu aujourd’hui",
- "taskAssign": "Confirmer les documents du conducteur",
- "taskInspect": "Finaliser l’inspection de retour"
- },
- "tour": {
- "title": "Visite du produit",
- "intro": "Suivez une location de la réservation au retour.",
- "back": "Précédent",
- "next": "Suivant",
- "finish": "Terminer la visite",
- "close": "Fermer la visite du produit",
- "step": "Étape",
- "of": "sur"
- },
- "research": {
- "title": "Mode recherche",
- "events": "événements",
- "export": "Exporter les événements",
- "clear": "Effacer",
- "close": "Fermer"
- }
-}
diff --git a/homepage/src/content/locales/fr/shell.json b/homepage/src/content/locales/fr/shell.json
deleted file mode 100644
index 5ecc868..0000000
--- a/homepage/src/content/locales/fr/shell.json
+++ /dev/null
@@ -1,112 +0,0 @@
-{
- "skipToContent": "Aller au contenu principal",
- "brandLabel": "Accueil RentalDriveGo",
- "foundationLabel": "Fondation de l’application",
- "header": {
- "navigationLabel": "Navigation principale",
- "controlsLabel": "Commandes d’affichage et de langue",
- "menuOpen": "Ouvrir le menu de navigation",
- "menuClose": "Fermer le menu de navigation",
- "nav": {
- "product": "Produit",
- "workflow": "Parcours",
- "modules": "Modules",
- "pricing": "Tarifs",
- "faq": "FAQ"
- },
- "login": "Se connecter",
- "loginUnavailable": "Destination de connexion en attente d’approbation",
- "demo": "Créer un compte",
- "demoUnavailable": "Création de compte en attente d’approbation de l’intégration"
- },
- "controls": {
- "language": "Langue",
- "theme": "Thème",
- "themeStatus": "Préférence de thème actuelle : {value}",
- "localeNames": {
- "en": "Anglais",
- "fr": "Français",
- "ar": "Arabe"
- },
- "themes": {
- "light": "Clair",
- "dark": "Sombre",
- "system": "Système"
- }
- },
- "footer": {
- "landmarkLabel": "Pied de page du site",
- "tagline": "Des opérations de location connectées, de la réservation au retour.",
- "groups": {
- "product": "Produit",
- "company": "Entreprise",
- "account": "Compte",
- "legal": "Mentions légales"
- },
- "links": {
- "workflow": "Parcours",
- "modules": "Modules",
- "pricing": "Tarifs",
- "faq": "FAQ",
- "contact": "Contacter le service commercial",
- "demo": "Créer un compte",
- "login": "Se connecter",
- "privacy": "Confidentialité",
- "terms": "Conditions",
- "accessibility": "Accessibilité"
- },
- "pending": "En attente d’approbation",
- "copyrightPrefix": "©",
- "releaseNotice": "La publication reste bloquée jusqu’à la résolution des obstacles documentés."
- },
- "states": {
- "loadingTitle": "Chargement",
- "loadingBody": "Préparation de la structure de l’application.",
- "errorTitle": "Une erreur s’est produite",
- "errorBody": "La page n’a pas pu être affichée. Aucun détail technique n’a été exposé.",
- "retry": "Réessayer",
- "notFoundTitle": "Page introuvable",
- "notFoundBody": "Cette adresse ne correspond à aucune route localisée approuvée.",
- "backHome": "Retour à l’accueil",
- "pendingTitle": "Destination indisponible",
- "pendingBody": "Cette route est définie, mais son contenu ou sa destination de production attend encore une approbation."
- },
- "foundation": {
- "eyebrow": "Fondation de la phase 11",
- "title": "Une structure localisée compatible avec les thèmes",
- "body": "Cette implémentation valide les fondations visuelles, de routage, de direction, d’accessibilité et de métadonnées sans prétendre que la page marketing complète existe déjà.",
- "readinessLabel": "Fondations implémentées",
- "items": [
- [
- "Jetons sémantiques",
- "Les styles des composants consomment une couche sémantique documentée."
- ],
- [
- "Gestion des thèmes",
- "Les préférences claire, sombre et système sont résolues et conservées."
- ],
- [
- "Localisation",
- "L’anglais, le français et l’arabe utilisent des routes préfixées par la langue."
- ],
- [
- "Direction",
- "L’arabe utilise une direction droite-à-gauche au niveau du document."
- ],
- [
- "Accessibilité",
- "Le lien d’évitement, le focus visible, les repères et le mouvement réduit sont intégrés."
- ],
- [
- "Structure de l’application",
- "L’en-tête adaptatif, la navigation mobile, le pied de page et les états globaux sont disponibles."
- ]
- ],
- "deferredLabel": "Destinations de la page d’accueil différées",
- "deferredBody": "Cette destination de navigation approuvée est reliée au manifeste de routes, tandis que sa section de production reste hors du périmètre de la phase 11."
- },
- "metadata": {
- "pendingTitle": "Destination indisponible | RentalDriveGo",
- "pendingDescription": "Cette destination localisée de RentalDriveGo est définie, mais sa publication en production n’est pas approuvée."
- }
-}
diff --git a/homepage/src/contracts/phase9/analytics-contract.ts b/homepage/src/contracts/phase9/analytics-contract.ts
deleted file mode 100644
index 4df44fd..0000000
--- a/homepage/src/contracts/phase9/analytics-contract.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-export type AnalyticsEventName =
- | 'page_view'
- | 'section_view'
- | 'navigation_click'
- | 'locale_change'
- | 'theme_change'
- | 'mobile_nav_open'
- | 'mobile_nav_close'
- | 'product_tour_open'
- | 'product_tour_step'
- | 'product_tour_complete'
- | 'product_tour_close'
- | 'demo_open'
- | 'demo_submit_attempt'
- | 'demo_validation_error'
- | 'demo_submit_success'
- | 'demo_submit_failure'
- | 'demo_close'
- | 'faq_open';
-export interface AnalyticsContext {
- routeId: string;
- locale: 'en' | 'fr' | 'ar';
- direction: 'ltr' | 'rtl';
- resolvedTheme: 'light' | 'dark';
- source?: string;
- step?: number;
- errorCode?: string;
-}
-export interface AnalyticsAdapter {
- consentGranted(): boolean;
- track(name: AnalyticsEventName, context: AnalyticsContext): void;
-}
-export const forbiddenPayloadKeys = [
- 'name',
- 'email',
- 'company',
- 'message',
- 'phone',
- 'fieldValue',
- 'serverResponse',
-] as const;
diff --git a/homepage/src/contracts/phase9/analytics-event-spec.csv b/homepage/src/contracts/phase9/analytics-event-spec.csv
deleted file mode 100644
index 5b7c4ee..0000000
--- a/homepage/src/contracts/phase9/analytics-event-spec.csv
+++ /dev/null
@@ -1,19 +0,0 @@
-Event,Trigger,Properties,PII_Allowed,Purpose
-page_view,Prototype loaded,"locale,theme,path,hash,session_id,test_mode,referrer_origin",No,Session start and denominator
-section_view,Section reaches 35% visibility,"section,locale,theme,hash,session_id",No,Discovery sequence
-navigation_click,Primary/mobile nav link used,"target,locale,theme,hash,session_id",No,Navigation path
-product_tour_open,Tour opened,"locale,theme,hash,session_id",No,Tour discovery
-product_tour_step,Tour step rendered,"step,locale,theme,session_id",No,Progress/drop-off
-product_tour_complete,Final tour step completed,"locale,theme,session_id",No,Completion
-product_tour_close,Tour closed,"reason,last_step,locale,theme,session_id",No,Abandonment point
-locale_change,Language selected,"from,to,context,session_id",No,Locale use/context preservation
-theme_change,Theme selected,"from,to,locale,session_id",No,Theme use
-mobile_nav_open,Drawer opened,"locale,theme,session_id",No,Mobile discovery
-mobile_nav_close,Drawer closed,"locale,theme,session_id",No,Drawer completion
-demo_open,Demo opened,"source,locale,theme,session_id",No,CTA source
-demo_validation_error,Validation blocked submit,"source,error_count,locale,theme,session_id",No,Error rate; never field values
-demo_submit_attempt,Submit activated,"source,locale,theme,session_id",No,Funnel attempt
-demo_submit_failure,Simulated service failure,"source,scenario,locale,theme,session_id",No,Failure recovery
-demo_submit_success,Prototype confirmation reached,"source,locale,theme,session_id",No,Completion
-demo_close,Demo closed,"reason,locale,theme,session_id",No,Abandonment
-faq_open,FAQ disclosure opened,"index,locale,theme,session_id",No,Question discovery
diff --git a/homepage/src/contracts/phase9/component-contracts.ts b/homepage/src/contracts/phase9/component-contracts.ts
deleted file mode 100644
index 24b527e..0000000
--- a/homepage/src/contracts/phase9/component-contracts.ts
+++ /dev/null
@@ -1,136 +0,0 @@
-export type Locale = 'en' | 'fr' | 'ar';
-export type Direction = 'ltr' | 'rtl';
-export type EvidenceStatus = 'approved' | 'illustrative' | 'pending' | 'expired' | 'rejected';
-export type ThemePreference = 'light' | 'dark' | 'system';
-
-export interface LinkModel {
- id: string;
- label: string;
- href: string;
- external?: boolean;
-}
-export interface SiteHeaderProps {
- locale: Locale;
- direction: Direction;
- links: LinkModel[];
- login?: LinkModel;
- onDemo(): void;
-}
-export interface DrawerProps extends SiteHeaderProps {
- open: boolean;
- onOpenChange(open: boolean): void;
- returnFocusRef?: HTMLElement | null;
-}
-export interface LocaleSelectorProps {
- locale: Locale;
- routeId: string;
- hash?: string;
- onLocaleChange(locale: Locale): void;
-}
-export interface ThemeSelectorProps {
- preference: ThemePreference;
- onPreferenceChange(value: ThemePreference): void;
-}
-export interface HeroModel {
- eyebrow: string;
- title: string;
- body: string;
- primaryLabel: string;
- secondaryLabel: string;
-}
-export interface ProductPreviewModel {
- status: EvidenceStatus;
- caption: string;
- locale: Locale;
- direction: Direction;
- theme: 'light' | 'dark';
- assetId?: string;
-}
-export interface LifecycleItem {
- id: string;
- title: string;
- description: string;
- icon: string;
-}
-export interface ComparisonModel {
- title: string;
- body: string;
- before: { title: string; items: string[] };
- after: { title: string; items: string[] };
-}
-export interface WorkflowStep {
- id: string;
- ordinal: string;
- title: string;
- description: string;
-}
-export interface RoleCard {
- id: string;
- title: string;
- description: string;
-}
-export interface ModuleCard {
- id: string;
- title: string;
- description: string;
- evidenceStatus: EvidenceStatus;
-}
-export interface MetricDefinition {
- id: string;
- title: string;
- definition: string;
- publishedValue?: never;
-}
-export interface IntegrationCategory {
- id: string;
- label: string;
- verifiedPartners?: never;
-}
-export interface SecurityCheck {
- id: string;
- title: string;
- description: string;
- certificationBadge?: never;
-}
-export interface PricingModel {
- mode: 'quote-only' | 'approved-pricing';
- title: string;
- body: string;
- factors: string[];
- note: string;
-}
-export interface FaqItem {
- id: string;
- question: string;
- answer: string;
-}
-export interface DemoFormValues {
- fullName: string;
- workEmail: string;
- company: string;
- fleetSize: string;
- market?: string;
- preferredLanguage?: Locale;
- message?: string;
-}
-export type DemoFormState =
- | 'idle'
- | 'validating'
- | 'submitting'
- | 'success'
- | 'recoverable_error'
- | 'terminal_error';
-export interface DemoFormProps {
- state: DemoFormState;
- values: DemoFormValues;
- fieldErrors: Partial>;
- formError?: string;
- onSubmit(values: DemoFormValues): Promise;
-}
-export interface TourProps {
- steps: WorkflowStep[];
- activeIndex: number;
- open: boolean;
- onOpenChange(open: boolean): void;
- onStepChange(index: number): void;
-}
diff --git a/homepage/src/contracts/phase9/demo-form-contract.ts b/homepage/src/contracts/phase9/demo-form-contract.ts
deleted file mode 100644
index 2e7b66e..0000000
--- a/homepage/src/contracts/phase9/demo-form-contract.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-export const fleetSizeValues = ['1-9', '10-24', '25-49', '50-99', '100-249', '250+'] as const;
-export type FleetSize = (typeof fleetSizeValues)[number];
-export interface DemoRequest {
- fullName: string;
- workEmail: string;
- company: string;
- fleetSize: FleetSize;
- market?: string;
- preferredLanguage?: 'en' | 'fr' | 'ar';
- message?: string;
- idempotencyKey: string;
- source: string;
-}
-export interface DemoSuccess {
- requestId: string;
- acceptedAt: string;
- nextStepCode: 'sales_followup' | 'manual_review';
-}
-export interface DemoFailure {
- code: 'validation' | 'rate_limited' | 'duplicate' | 'unavailable' | 'unknown';
- retryable: boolean;
- fieldErrors?: Record;
-}
diff --git a/homepage/src/contracts/phase9/design-tokens.json b/homepage/src/contracts/phase9/design-tokens.json
deleted file mode 100644
index 03e916f..0000000
--- a/homepage/src/contracts/phase9/design-tokens.json
+++ /dev/null
@@ -1,150 +0,0 @@
-{
- "$schema": "https://design-tokens.github.io/community-group/format/",
- "version": "1.0",
- "raw": {
- "color": {
- "blue": {
- "50": "#eff6ff",
- "100": "#dbeafe",
- "200": "#bfdbfe",
- "500": "#2874e8",
- "600": "#1b5dd8",
- "700": "#174bb5",
- "800": "#183d88"
- },
- "orange": {
- "100": "#ffedd5",
- "400": "#fb923c",
- "600": "#ea580c",
- "700": "#c2410c"
- },
- "navy": {
- "950": "#081426",
- "900": "#0d1b2e",
- "850": "#11233b",
- "800": "#162b46"
- },
- "gray": {
- "25": "#fbfdff",
- "50": "#f7f9fc",
- "100": "#eef2f7",
- "200": "#dbe3ed",
- "300": "#c6d0dc",
- "500": "#64748b",
- "600": "#475569",
- "700": "#334155",
- "800": "#1e293b",
- "900": "#0f172a"
- },
- "green": {
- "100": "#dcfce7",
- "700": "#15803d"
- },
- "amber": {
- "100": "#fef3c7",
- "800": "#92400e"
- },
- "red": {
- "100": "#fee2e2",
- "700": "#b91c1c"
- }
- },
- "space": {
- "0": "0px",
- "1": "4px",
- "2": "8px",
- "3": "12px",
- "4": "16px",
- "5": "20px",
- "6": "24px",
- "7": "28px",
- "8": "32px",
- "9": "36px",
- "10": "40px",
- "11": "44px",
- "12": "48px",
- "13": "52px",
- "14": "56px",
- "15": "60px",
- "16": "64px",
- "17": "68px",
- "18": "72px",
- "19": "76px",
- "20": "80px",
- "21": "84px",
- "22": "88px",
- "23": "92px",
- "24": "96px"
- },
- "radius": {
- "sm": "10px",
- "md": "16px",
- "lg": "24px",
- "xl": "32px",
- "pill": "999px"
- },
- "size": {
- "touch-min": "44px",
- "container-max": "1280px"
- },
- "duration": {
- "fast": "120ms",
- "standard": "160ms",
- "overlay": "220ms"
- },
- "easing": {
- "standard": "ease",
- "productive": "cubic-bezier(.2,0,0,1)"
- },
- "font": {
- "latin": "Inter Variable, Inter, Arial, sans-serif",
- "arabic": "Noto Sans Arabic, Arial, sans-serif"
- },
- "breakpoint": {
- "medium": "768px",
- "wide": "1120px"
- }
- },
- "semantic": {
- "light": {
- "color.canvas": "{color.gray.25}",
- "color.surface": "#ffffff",
- "color.surface.raised": "#ffffff",
- "color.surface.muted": "#f2f6fb",
- "color.surface.strong": "#e8f0fa",
- "color.text": "#102035",
- "color.text.muted": "#52647a",
- "color.border": "#d8e2ee",
- "color.border.strong": "#bdcad9",
- "color.action.primary": "{color.blue.700}",
- "color.action.primary.hover": "{color.blue.800}",
- "color.action.conversion": "{color.orange.700}",
- "color.action.conversion.hover": "#9a3412",
- "color.focus": "#f97316",
- "color.on.inverse": "#f8fafc",
- "color.soft.blue": "#eef5ff",
- "color.soft.orange": "#fff7ed",
- "shadow.color": "rgba(8,20,38,.12)"
- },
- "dark": {
- "color.canvas": "{color.navy.950}",
- "color.surface": "{color.navy.900}",
- "color.surface.raised": "{color.navy.850}",
- "color.surface.muted": "#0b1a2d",
- "color.surface.strong": "#172d48",
- "color.text": "#eef5ff",
- "color.text.muted": "#adbed2",
- "color.border": "#263c56",
- "color.border.strong": "#3a536e",
- "color.action.primary": "#76a9ff",
- "color.action.primary.hover": "#9bc2ff",
- "color.action.conversion": "{color.orange.400}",
- "color.action.conversion.hover": "#fdba74",
- "color.focus": "{color.orange.400}",
- "color.on.inverse": "{color.navy.950}",
- "color.soft.blue": "#10284a",
- "color.soft.orange": "#3a2015",
- "shadow.color": "rgba(0,0,0,.36)"
- }
- }
-}
diff --git a/homepage/src/contracts/phase9/performance-budgets.json b/homepage/src/contracts/phase9/performance-budgets.json
deleted file mode 100644
index d0639f3..0000000
--- a/homepage/src/contracts/phase9/performance-budgets.json
+++ /dev/null
@@ -1,28 +0,0 @@
-{
- "version": "1.0",
- "measurement": "75th percentile real-user data where available; otherwise repeatable lab median on defined mobile profile",
- "core": {
- "LCP_ms_max": 2500,
- "INP_ms_max": 200,
- "CLS_max": 0.1,
- "TTFB_ms_max": 800
- },
- "transfer_gzip": {
- "initial_javascript_kb_max": 180,
- "initial_css_kb_max": 60,
- "initial_html_kb_max": 100,
- "hero_media_kb_max": 250,
- "font_total_kb_max": 220
- },
- "requests": {
- "initial_third_party_max": 2,
- "initial_font_files_max": 2
- },
- "rules": [
- "No marketing tag may block first paint.",
- "Load analytics after consent and idle where possible.",
- "Reserve media dimensions to prevent layout shift.",
- "No autoplay video on initial load.",
- "Test EN, FR, and AR because font and copy costs differ."
- ]
-}
diff --git a/homepage/src/contracts/phase9/route-manifest.json b/homepage/src/contracts/phase9/route-manifest.json
deleted file mode 100644
index 1d1aee8..0000000
--- a/homepage/src/contracts/phase9/route-manifest.json
+++ /dev/null
@@ -1,88 +0,0 @@
-{
- "version": "1.0",
- "defaultLocale": "en",
- "locales": ["en", "fr", "ar"],
- "rootDetection": true,
- "routes": [
- {
- "id": "home",
- "slugs": {
- "en": "",
- "fr": "",
- "ar": ""
- },
- "indexable": true,
- "hashes": ["top", "product", "workflow", "modules", "pricing", "faq", "contact"]
- },
- {
- "id": "sign-in",
- "slugs": {
- "en": "sign-in",
- "fr": "connexion",
- "ar": "تسجيل-الدخول"
- },
- "indexable": false,
- "hashes": []
- },
- {
- "id": "forgot-password",
- "slugs": {
- "en": "forgot-password",
- "fr": "mot-de-passe-oublie",
- "ar": "نسيت-كلمة-المرور"
- },
- "indexable": false,
- "hashes": []
- },
- {
- "id": "reset-password",
- "slugs": {
- "en": "reset-password",
- "fr": "reinitialiser-mot-de-passe",
- "ar": "اعادة-تعيين-كلمة-المرور"
- },
- "indexable": false,
- "hashes": []
- },
- {
- "id": "privacy",
- "slugs": {
- "en": "privacy",
- "fr": "confidentialite",
- "ar": "الخصوصية"
- },
- "indexable": true,
- "hashes": []
- },
- {
- "id": "terms",
- "slugs": {
- "en": "terms",
- "fr": "conditions",
- "ar": "الشروط"
- },
- "indexable": true,
- "hashes": []
- },
- {
- "id": "accessibility",
- "slugs": {
- "en": "accessibility",
- "fr": "accessibilite",
- "ar": "إمكانية-الوصول"
- },
- "indexable": true,
- "hashes": []
- }
- ],
- "switching": {
- "preserveQueryAllowlist": ["utm_source", "utm_medium", "utm_campaign", "utm_content", "gclid"],
- "preserveValidHash": true,
- "dropUnknownQuery": true
- },
- "hreflang": {
- "values": ["en", "fr", "ar"],
- "xDefaultLocale": "en",
- "selfCanonical": true
- }
-}
diff --git a/homepage/src/contracts/phase9/visual-regression-scenarios.json b/homepage/src/contracts/phase9/visual-regression-scenarios.json
deleted file mode 100644
index c11dbc2..0000000
--- a/homepage/src/contracts/phase9/visual-regression-scenarios.json
+++ /dev/null
@@ -1,62 +0,0 @@
-{
- "version": "1.0",
- "pixelDiffThreshold": 0.01,
- "scenarios": [
- {
- "id": "VR-001",
- "route": "/en",
- "viewport": [1440, 1000],
- "theme": "light",
- "state": "homepage-top"
- },
- {
- "id": "VR-002",
- "route": "/fr",
- "viewport": [1024, 900],
- "theme": "dark",
- "state": "homepage-top"
- },
- {
- "id": "VR-003",
- "route": "/ar",
- "viewport": [390, 844],
- "theme": "light",
- "state": "homepage-top"
- },
- {
- "id": "VR-004",
- "route": "/ar",
- "viewport": [390, 844],
- "theme": "dark",
- "state": "drawer-open"
- },
- {
- "id": "VR-005",
- "route": "/en",
- "viewport": [390, 844],
- "theme": "light",
- "state": "demo-invalid"
- },
- {
- "id": "VR-006",
- "route": "/fr",
- "viewport": [768, 1024],
- "theme": "dark",
- "state": "tour-step-3"
- },
- {
- "id": "VR-007",
- "route": "/ar",
- "viewport": [320, 800],
- "theme": "dark",
- "state": "reflow-gate"
- },
- {
- "id": "VR-008",
- "route": "/en",
- "viewport": [1440, 1000],
- "theme": "dark",
- "state": "pricing-and-final-cta"
- }
- ]
-}
diff --git a/homepage/src/lib/analytics/client.ts b/homepage/src/lib/analytics/client.ts
deleted file mode 100644
index a2c4b92..0000000
--- a/homepage/src/lib/analytics/client.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-'use client';
-
-import { validateAnalyticsEvent, type AnalyticsContext, type AnalyticsEventName } from './events';
-
-declare global {
- interface Window {
- __rdgAnalyticsTestSink?: Array<{ name: AnalyticsEventName; context: AnalyticsContext }>;
- }
-}
-
-function analyticsMode(): 'disabled' | 'test' {
- return process.env.NEXT_PUBLIC_ANALYTICS_MODE === 'test' ? 'test' : 'disabled';
-}
-
-function consentGranted(): boolean {
- return document.documentElement.dataset.analyticsConsent === 'granted';
-}
-
-export function trackAnalytics(name: AnalyticsEventName, context: AnalyticsContext): boolean {
- const event = validateAnalyticsEvent(name, context);
- if (!event || analyticsMode() !== 'test' || !consentGranted()) return false;
- window.__rdgAnalyticsTestSink ??= [];
- window.__rdgAnalyticsTestSink.push(event);
- return true;
-}
diff --git a/homepage/src/lib/analytics/events.ts b/homepage/src/lib/analytics/events.ts
deleted file mode 100644
index 8d15662..0000000
--- a/homepage/src/lib/analytics/events.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-import { z } from 'zod';
-
-export const analyticsEventNames = [
- 'page_view',
- 'section_view',
- 'navigation_click',
- 'locale_change',
- 'theme_change',
- 'mobile_nav_open',
- 'mobile_nav_close',
- 'product_tour_open',
- 'product_tour_step',
- 'product_tour_complete',
- 'product_tour_close',
- 'demo_open',
- 'demo_submit_attempt',
- 'demo_validation_error',
- 'demo_submit_success',
- 'demo_submit_failure',
- 'demo_close',
- 'faq_open',
-] as const;
-
-export type AnalyticsEventName = (typeof analyticsEventNames)[number];
-
-const safeText = z
- .string()
- .min(1)
- .max(80)
- .regex(/^[a-zA-Z0-9_-]+$/);
-
-export const analyticsContextSchema = z
- .object({
- routeId: safeText,
- locale: z.enum(['en', 'fr', 'ar']),
- direction: z.enum(['ltr', 'rtl']),
- resolvedTheme: z.enum(['light', 'dark']),
- source: safeText.optional(),
- step: z.number().int().min(1).max(100).optional(),
- errorCode: safeText.optional(),
- errorCount: z.number().int().min(1).max(20).optional(),
- reason: safeText.optional(),
- })
- .strict();
-
-export type AnalyticsContext = z.output;
-
-export interface AnalyticsEventDefinition {
- name: AnalyticsEventName;
- purpose: string;
- consentCategory: 'analytics';
- personalData: false;
-}
-
-export const analyticsEventRegistry: readonly AnalyticsEventDefinition[] = analyticsEventNames.map(
- (name) => ({
- name,
- purpose: `Phase 9 approved ${name} interaction measurement.`,
- consentCategory: 'analytics',
- personalData: false,
- }),
-);
-
-const forbiddenPayloadKeys = new Set([
- 'name',
- 'email',
- 'company',
- 'message',
- 'phone',
- 'fieldvalue',
- 'serverresponse',
-]);
-
-export function validateAnalyticsEvent(name: unknown, context: unknown) {
- const event = z.enum(analyticsEventNames).safeParse(name);
- const payload = analyticsContextSchema.safeParse(context);
- if (!event.success || !payload.success) return null;
- if (Object.keys(payload.data).some((key) => forbiddenPayloadKeys.has(key.toLowerCase()))) {
- return null;
- }
- return { name: event.data, context: payload.data } as const;
-}
diff --git a/homepage/src/lib/api.ts b/homepage/src/lib/api.ts
deleted file mode 100644
index f91fe1b..0000000
--- a/homepage/src/lib/api.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export const API_BASE =
- typeof window === 'undefined'
- ? (process.env.API_INTERNAL_URL || 'http://localhost:4000/api/v1')
- : (process.env.NEXT_PUBLIC_API_URL || '/api/v1')
-
-export const EMPLOYEE_PROFILE_KEY = 'employee_profile'
diff --git a/homepage/src/lib/components/classNames.ts b/homepage/src/lib/components/classNames.ts
deleted file mode 100644
index 582dbc1..0000000
--- a/homepage/src/lib/components/classNames.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export function classNames(...values: Array): string {
- return values.filter(Boolean).join(' ');
-}
diff --git a/homepage/src/lib/demo/adapters/blocked.ts b/homepage/src/lib/demo/adapters/blocked.ts
deleted file mode 100644
index b921783..0000000
--- a/homepage/src/lib/demo/adapters/blocked.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import type { LeadDestinationAdapter, LeadDestinationResult } from './types';
-
-export class BlockedLeadDestinationAdapter implements LeadDestinationAdapter {
- readonly name = 'blocked';
-
- async submitLead(): Promise {
- return {
- ok: false,
- category: 'configuration',
- retryable: false,
- code: 'lead_destination_not_approved',
- };
- }
-}
diff --git a/homepage/src/lib/demo/adapters/index.ts b/homepage/src/lib/demo/adapters/index.ts
deleted file mode 100644
index 66ecc26..0000000
--- a/homepage/src/lib/demo/adapters/index.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import type { IntegrationEnvironment } from '@/lib/integrations/environment';
-import { BlockedLeadDestinationAdapter } from './blocked';
-import { LocalLeadDestinationAdapter } from './local';
-import type { LeadDestinationAdapter } from './types';
-
-export function createLeadDestinationAdapter(
- environment: IntegrationEnvironment,
-): LeadDestinationAdapter {
- const approvedLocalRuntime =
- environment.NODE_ENV !== 'production' || environment.DEMO_LOCAL_TEST_MODE;
- if (approvedLocalRuntime && environment.DEMO_SUBMISSION_MODE === 'local') {
- return new LocalLeadDestinationAdapter();
- }
- return new BlockedLeadDestinationAdapter();
-}
-
-export type { IntegrationContext, LeadDestinationAdapter, LeadDestinationResult } from './types';
diff --git a/homepage/src/lib/demo/adapters/local.ts b/homepage/src/lib/demo/adapters/local.ts
deleted file mode 100644
index 3d3142d..0000000
--- a/homepage/src/lib/demo/adapters/local.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import type { LeadDestinationAdapter, LeadDestinationResult } from './types';
-
-function abortError(): Error {
- return new DOMException('Operation aborted', 'AbortError');
-}
-
-export class LocalLeadDestinationAdapter implements LeadDestinationAdapter {
- readonly name = 'local-discard';
-
- async submitLead(
- _lead: Parameters[0],
- context: Parameters[1],
- ): Promise {
- if (context.signal.aborted) throw abortError();
-
- switch (context.testScenario) {
- case 'duplicate':
- return { ok: false, category: 'duplicate', retryable: false, code: 'local_duplicate' };
- case 'integration-error':
- return { ok: false, category: 'integration', retryable: true, code: 'local_error' };
- case 'timeout':
- await new Promise((resolve, reject) => {
- const timer = setTimeout(resolve, 30_000);
- context.signal.addEventListener(
- 'abort',
- () => {
- clearTimeout(timer);
- reject(abortError());
- },
- { once: true },
- );
- });
- break;
- case 'success':
- case undefined:
- break;
- }
-
- return {
- ok: true,
- externalId: `local_${context.correlationId}`,
- acceptedAt: new Date().toISOString(),
- mode: 'local',
- };
- }
-}
diff --git a/homepage/src/lib/demo/adapters/types.ts b/homepage/src/lib/demo/adapters/types.ts
deleted file mode 100644
index 8ba56e4..0000000
--- a/homepage/src/lib/demo/adapters/types.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import type { DemoLead } from '../schema';
-
-export interface IntegrationContext {
- correlationId: string;
- idempotencyKey: string;
- signal: AbortSignal;
- environment: 'development' | 'test' | 'production';
- testScenario?: 'success' | 'duplicate' | 'timeout' | 'integration-error';
-}
-
-export type LeadDestinationResult =
- | { ok: true; externalId: string; acceptedAt: string; mode: 'local' }
- | {
- ok: false;
- category: 'duplicate' | 'integration' | 'timeout' | 'configuration';
- retryable: boolean;
- code: string;
- };
-
-export interface LeadDestinationAdapter {
- readonly name: string;
- submitLead(lead: DemoLead, context: IntegrationContext): Promise;
-}
diff --git a/homepage/src/lib/demo/idempotency.ts b/homepage/src/lib/demo/idempotency.ts
deleted file mode 100644
index 993231f..0000000
--- a/homepage/src/lib/demo/idempotency.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import type { DemoSubmissionResult } from './schema';
-
-interface StoredOperation {
- expiresAt: number;
- result: Promise;
-}
-
-const globalStore = globalThis as typeof globalThis & {
- __rdgDemoIdempotencyStore?: Map;
-};
-
-const store = globalStore.__rdgDemoIdempotencyStore ?? new Map();
-globalStore.__rdgDemoIdempotencyStore = store;
-
-function purgeExpired(now: number) {
- for (const [key, operation] of store) {
- if (operation.expiresAt <= now) store.delete(key);
- }
-}
-
-export async function executeIdempotent(
- key: string,
- ttlSeconds: number,
- operation: () => Promise,
-): Promise {
- const now = Date.now();
- purgeExpired(now);
- const existing = store.get(key);
- if (existing) return existing.result;
-
- const result = operation();
- store.set(key, { expiresAt: now + ttlSeconds * 1_000, result });
-
- try {
- const settled = await result;
- if (!settled.ok && settled.retryable) store.delete(key);
- return settled;
- } catch (error) {
- store.delete(key);
- throw error;
- }
-}
-
-export function clearIdempotencyStoreForTests() {
- store.clear();
-}
diff --git a/homepage/src/lib/demo/redaction.ts b/homepage/src/lib/demo/redaction.ts
deleted file mode 100644
index c69b4eb..0000000
--- a/homepage/src/lib/demo/redaction.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-const sensitiveKeys = new Set([
- 'fullname',
- 'name',
- 'workemail',
- 'email',
- 'company',
- 'market',
- 'message',
- 'phone',
- 'fieldvalue',
- 'serverresponse',
-]);
-
-export function redactSensitive(value: unknown): unknown {
- if (Array.isArray(value)) return value.map(redactSensitive);
- if (!value || typeof value !== 'object') return value;
-
- return Object.fromEntries(
- Object.entries(value).map(([key, child]) => [
- key,
- sensitiveKeys.has(key.toLowerCase()) ? '[REDACTED]' : redactSensitive(child),
- ]),
- );
-}
-
-export function containsSensitiveKey(value: unknown): boolean {
- if (Array.isArray(value)) return value.some(containsSensitiveKey);
- if (!value || typeof value !== 'object') return false;
- return Object.entries(value).some(
- ([key, child]) => sensitiveKeys.has(key.toLowerCase()) || containsSensitiveKey(child),
- );
-}
diff --git a/homepage/src/lib/demo/schema.ts b/homepage/src/lib/demo/schema.ts
deleted file mode 100644
index a1c82b8..0000000
--- a/homepage/src/lib/demo/schema.ts
+++ /dev/null
@@ -1,94 +0,0 @@
-import { z } from 'zod';
-
-export const fleetSizeValues = ['1-9', '10-24', '25-49', '50-99', '100-249', '250+'] as const;
-export const demoSourceValues = ['header', 'mobile-header', 'hero', 'final-cta', 'footer'] as const;
-
-const compactText = (maximum: number) =>
- z
- .string()
- .transform((value) => value.trim().replace(/\s+/g, ' '))
- .pipe(z.string().min(1, 'required').max(maximum, 'too_long'));
-
-const optionalCompactText = (maximum: number) =>
- z
- .string()
- .transform((value) => value.trim().replace(/\s+/g, ' '))
- .pipe(z.string().max(maximum, 'too_long'))
- .transform((value) => (value.length === 0 ? undefined : value))
- .optional();
-
-export const demoLeadSchema = z
- .object({
- fullName: compactText(120),
- workEmail: z
- .string()
- .transform((value) => value.trim().toLowerCase())
- .pipe(
- z
- .string()
- .min(1, 'required')
- .max(254, 'too_long')
- .regex(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, 'invalid_email'),
- ),
- company: compactText(160),
- fleetSize: z.enum(fleetSizeValues, { error: 'invalid_fleet_size' }),
- market: optionalCompactText(100),
- preferredLanguage: z.enum(['en', 'fr', 'ar']).optional(),
- message: optionalCompactText(1_000),
- idempotencyKey: z.string().uuid('invalid_idempotency_key'),
- source: z.enum(demoSourceValues, { error: 'invalid_source' }),
- })
- .strict();
-
-export const demoSubmissionEnvelopeSchema = z
- .object({
- lead: demoLeadSchema,
- guard: z
- .object({
- honeypot: z.string().max(200).default(''),
- startedAtMs: z.number().int().nonnegative(),
- })
- .strict(),
- })
- .strict();
-
-export type DemoLeadInput = z.input;
-export type DemoLead = z.output;
-export type DemoSubmissionEnvelope = z.output;
-export type DemoSource = (typeof demoSourceValues)[number];
-
-export type DemoFailureCategory =
- | 'validation'
- | 'consent'
- | 'duplicate'
- | 'rate-limit'
- | 'integration'
- | 'timeout'
- | 'configuration'
- | 'abuse';
-
-export type DemoSubmissionResult =
- | {
- ok: true;
- submissionId: string;
- acceptedAt: string;
- nextAction: { type: 'success'; mode: 'local' };
- }
- | {
- ok: false;
- category: DemoFailureCategory;
- fieldErrors?: Record;
- formErrorCode: string;
- retryable: boolean;
- correlationId?: string;
- };
-
-export function zodFieldErrors(error: z.ZodError): Record {
- const result: Record = {};
- for (const issue of error.issues) {
- const path = issue.path.at(-1);
- if (typeof path !== 'string' || result[path]) continue;
- result[path] = issue.message;
- }
- return result;
-}
diff --git a/homepage/src/lib/demo/service.ts b/homepage/src/lib/demo/service.ts
deleted file mode 100644
index 06860d7..0000000
--- a/homepage/src/lib/demo/service.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-import { randomUUID } from 'node:crypto';
-import type { IntegrationEnvironment } from '@/lib/integrations/environment';
-import { createLeadDestinationAdapter, type LeadDestinationAdapter } from './adapters';
-import { executeIdempotent } from './idempotency';
-import type { DemoLead, DemoSubmissionResult } from './schema';
-
-export interface SubmitDemoOptions {
- environment: IntegrationEnvironment;
- adapter?: LeadDestinationAdapter;
- testScenario?: 'success' | 'duplicate' | 'timeout' | 'integration-error';
-}
-
-export async function submitDemoLead(
- lead: DemoLead,
- options: SubmitDemoOptions,
-): Promise {
- const { environment } = options;
- const adapter = options.adapter ?? createLeadDestinationAdapter(environment);
-
- return executeIdempotent(
- lead.idempotencyKey,
- environment.DEMO_IDEMPOTENCY_TTL_SECONDS,
- async () => {
- const correlationId = randomUUID();
- const controller = new AbortController();
- const timeout = setTimeout(() => controller.abort(), environment.DEMO_REQUEST_TIMEOUT_MS);
-
- try {
- const destinationResult = await adapter.submitLead(lead, {
- correlationId,
- idempotencyKey: lead.idempotencyKey,
- signal: controller.signal,
- environment: environment.NODE_ENV,
- ...(options.testScenario ? { testScenario: options.testScenario } : {}),
- });
-
- if (!destinationResult.ok) {
- return {
- ok: false,
- category: destinationResult.category,
- formErrorCode: destinationResult.code,
- retryable: destinationResult.retryable,
- correlationId,
- };
- }
-
- return {
- ok: true,
- submissionId: destinationResult.externalId,
- acceptedAt: destinationResult.acceptedAt,
- nextAction: { type: 'success', mode: destinationResult.mode },
- };
- } catch (error) {
- const timedOut =
- controller.signal.aborted ||
- (error instanceof DOMException && error.name === 'AbortError');
- return {
- ok: false,
- category: timedOut ? 'timeout' : 'integration',
- formErrorCode: timedOut ? 'lead_destination_timeout' : 'lead_destination_failure',
- retryable: true,
- correlationId,
- };
- } finally {
- clearTimeout(timeout);
- }
- },
- );
-}
diff --git a/homepage/src/lib/integrations/destinations.ts b/homepage/src/lib/integrations/destinations.ts
deleted file mode 100644
index cb6faf6..0000000
--- a/homepage/src/lib/integrations/destinations.ts
+++ /dev/null
@@ -1,174 +0,0 @@
-import { localizedPath, type Locale, type RouteId } from '@/lib/localization/config';
-
-export type DestinationStatus = 'active' | 'disabled' | 'placeholder' | 'blocked';
-export type DestinationKind =
- | 'internal-route'
- | 'external-url'
- | 'modal'
- | 'drawer'
- | 'form'
- | 'scheduler';
-
-export type DestinationKey =
- | 'book-demo'
- | 'contact-sales'
- | 'product-tour'
- | 'pricing'
- | 'sign-in'
- | 'forgot-password'
- | 'privacy'
- | 'terms'
- | 'accessibility'
- | 'cookie-settings'
- | 'support';
-
-export interface DestinationReference {
- key: DestinationKey;
- kind: DestinationKind;
- status: DestinationStatus;
- routeId?: RouteId;
- href?: string;
- featureFlag?: string;
- issueId?: string;
- external?: boolean;
- allowedLocales: readonly Locale[];
- reason: string;
-}
-
-const allLocales = ['en', 'fr', 'ar'] as const satisfies readonly Locale[];
-
-const baseDestinations: Record = {
- 'book-demo': {
- key: 'book-demo',
- kind: 'modal',
- status: 'blocked',
- featureFlag: 'demoSubmissionEnabled',
- issueId: 'P9-002/P9-003',
- allowedLocales: allLocales,
- reason: 'Production CRM and legal consent decisions are not approved.',
- },
- 'contact-sales': {
- key: 'contact-sales',
- kind: 'external-url',
- status: 'blocked',
- issueId: 'P9-015',
- allowedLocales: allLocales,
- reason: 'No approved sales destination exists.',
- },
- 'product-tour': {
- key: 'product-tour',
- kind: 'modal',
- status: 'blocked',
- issueId: 'P9-012',
- allowedLocales: allLocales,
- reason: 'No approved product-tour medium or transcript exists.',
- },
- pricing: {
- key: 'pricing',
- kind: 'internal-route',
- status: 'placeholder',
- issueId: 'P9-008',
- allowedLocales: allLocales,
- reason: 'Public pricing and packaging are not approved.',
- },
- 'sign-in': {
- key: 'sign-in',
- kind: 'internal-route',
- status: 'active',
- routeId: 'sign-in',
- allowedLocales: allLocales,
- reason: 'Sign-in is now served from the homepage app.',
- },
- 'forgot-password': {
- key: 'forgot-password',
- kind: 'internal-route',
- status: 'active',
- routeId: 'forgot-password',
- allowedLocales: allLocales,
- reason: 'Password reset flow is now served from the homepage app.',
- },
- privacy: {
- key: 'privacy',
- kind: 'internal-route',
- status: 'placeholder',
- routeId: 'privacy',
- issueId: 'P9-003',
- allowedLocales: allLocales,
- reason: 'The privacy destination resolves to a gated placeholder pending legal copy.',
- },
- terms: {
- key: 'terms',
- kind: 'internal-route',
- status: 'placeholder',
- routeId: 'terms',
- issueId: 'P9-003',
- allowedLocales: allLocales,
- reason: 'The terms destination resolves to a gated placeholder pending legal copy.',
- },
- accessibility: {
- key: 'accessibility',
- kind: 'internal-route',
- status: 'placeholder',
- routeId: 'accessibility',
- issueId: 'P13-002',
- allowedLocales: allLocales,
- reason: 'The accessibility destination resolves to a gated placeholder pending approval.',
- },
- 'cookie-settings': {
- key: 'cookie-settings',
- kind: 'modal',
- status: 'blocked',
- issueId: 'P9-004',
- allowedLocales: allLocales,
- reason: 'Consent categories and a consent-management mechanism are not approved.',
- },
- support: {
- key: 'support',
- kind: 'external-url',
- status: 'blocked',
- issueId: 'P9-015',
- allowedLocales: allLocales,
- reason: 'No approved public support destination exists.',
- },
-};
-
-export interface DestinationRuntimeContext {
- demoSubmissionEnabled: boolean;
- demoMode: 'blocked' | 'local';
-}
-
-export function destinationRegistry(
- context: DestinationRuntimeContext,
-): Record {
- const demo = baseDestinations['book-demo'];
- return {
- ...baseDestinations,
- 'book-demo': {
- ...demo,
- status: context.demoSubmissionEnabled && context.demoMode === 'local' ? 'active' : 'blocked',
- reason:
- context.demoSubmissionEnabled && context.demoMode === 'local'
- ? 'Local validation adapter is active; no external lead is created.'
- : demo.reason,
- },
- };
-}
-
-export function resolveDestination(
- key: DestinationKey,
- locale: Locale,
- context: DestinationRuntimeContext,
-): DestinationReference {
- const destination = destinationRegistry(context)[key];
- if (!destination.allowedLocales.includes(locale)) {
- return {
- ...destination,
- status: 'blocked',
- reason: 'Destination is unavailable for this locale.',
- };
- }
- if (destination.routeId) {
- return { ...destination, href: localizedPath(destination.routeId, locale) };
- }
- return destination;
-}
diff --git a/homepage/src/lib/integrations/environment.ts b/homepage/src/lib/integrations/environment.ts
deleted file mode 100644
index 3daf280..0000000
--- a/homepage/src/lib/integrations/environment.ts
+++ /dev/null
@@ -1,119 +0,0 @@
-import 'server-only';
-
-import { z } from 'zod';
-
-const booleanText = z
- .enum(['true', 'false'])
- .default('false')
- .transform((value) => value === 'true');
-
-const environmentSchema = z.object({
- NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
- NEXT_PUBLIC_DEMO_ENABLED: booleanText,
- DEMO_SUBMISSION_MODE: z.enum(['blocked', 'local']).default('blocked'),
- DEMO_REQUEST_TIMEOUT_MS: z.coerce.number().int().min(250).max(15_000).default(4_000),
- DEMO_IDEMPOTENCY_TTL_SECONDS: z.coerce.number().int().min(60).max(86_400).default(900),
- DEMO_TEST_SCENARIOS_ENABLED: booleanText,
- DEMO_LOCAL_TEST_MODE: booleanText,
- NEXT_PUBLIC_ANALYTICS_MODE: z.enum(['disabled', 'test']).default('disabled'),
- SITE_ORIGIN: z.string().url().optional(),
-});
-
-export type IntegrationEnvironment = z.infer;
-
-export interface EnvironmentValidationResult {
- ok: boolean;
- errors: string[];
- environment?: IntegrationEnvironment;
-}
-
-export function validateIntegrationEnvironment(
- source: NodeJS.ProcessEnv = process.env,
-): EnvironmentValidationResult {
- const inferredNodeEnv =
- source.NODE_ENV === 'production' || source.NODE_ENV === 'test'
- ? source.NODE_ENV
- : 'development';
- const normalizedSource = {
- ...source,
- DEMO_SUBMISSION_MODE:
- source.DEMO_SUBMISSION_MODE ?? (inferredNodeEnv === 'production' ? 'blocked' : 'local'),
- };
- const parsed = environmentSchema.safeParse(normalizedSource);
- if (!parsed.success) {
- return {
- ok: false,
- errors: parsed.error.issues.map((issue) => `${issue.path.join('.')}: ${issue.message}`),
- };
- }
-
- const environment = parsed.data;
- const errors: string[] = [];
- const localTestOrigin =
- environment.SITE_ORIGIN &&
- ['127.0.0.1', 'localhost'].includes(new URL(environment.SITE_ORIGIN).hostname);
- const productionLocalTest =
- environment.NODE_ENV === 'production' && environment.DEMO_LOCAL_TEST_MODE && localTestOrigin;
- if (
- environment.NODE_ENV === 'production' &&
- environment.NEXT_PUBLIC_DEMO_ENABLED &&
- !productionLocalTest
- ) {
- errors.push(
- 'NEXT_PUBLIC_DEMO_ENABLED must remain false until an approved production lead adapter and legal consent decision are implemented.',
- );
- }
- if (
- environment.NODE_ENV === 'production' &&
- environment.DEMO_SUBMISSION_MODE === 'local' &&
- !productionLocalTest
- ) {
- errors.push(
- 'DEMO_SUBMISSION_MODE=local is prohibited outside an explicit loopback test runtime.',
- );
- }
- if (
- environment.NODE_ENV === 'production' &&
- environment.DEMO_TEST_SCENARIOS_ENABLED &&
- !productionLocalTest
- ) {
- errors.push(
- 'DEMO_TEST_SCENARIOS_ENABLED is prohibited outside an explicit loopback test runtime.',
- );
- }
- if (
- environment.NODE_ENV === 'production' &&
- environment.NEXT_PUBLIC_ANALYTICS_MODE !== 'disabled'
- ) {
- errors.push('Analytics must remain disabled until provider and consent approval are recorded.');
- }
-
- return errors.length > 0 ? { ok: false, errors, environment } : { ok: true, errors, environment };
-}
-
-export function getIntegrationEnvironment(): IntegrationEnvironment {
- const result = validateIntegrationEnvironment();
- if (!result.ok || !result.environment) {
- throw new Error(`Invalid Phase 14 integration environment: ${result.errors.join(' | ')}`);
- }
- return result.environment;
-}
-
-export interface PublicIntegrationConfig {
- demoSubmissionEnabled: boolean;
- demoMode: 'blocked' | 'local';
- analyticsMode: 'disabled' | 'test';
-}
-
-export function getPublicIntegrationConfig(): PublicIntegrationConfig {
- const environment = getIntegrationEnvironment();
- const developmentDefault = environment.NODE_ENV !== 'production';
- const demoSubmissionEnabled =
- environment.DEMO_SUBMISSION_MODE === 'local' &&
- (environment.NEXT_PUBLIC_DEMO_ENABLED || developmentDefault);
- return {
- demoSubmissionEnabled,
- demoMode: demoSubmissionEnabled ? 'local' : 'blocked',
- analyticsMode: environment.NEXT_PUBLIC_ANALYTICS_MODE,
- };
-}
diff --git a/homepage/src/lib/integrations/feature-flags.ts b/homepage/src/lib/integrations/feature-flags.ts
deleted file mode 100644
index e7d598a..0000000
--- a/homepage/src/lib/integrations/feature-flags.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import type { PublicIntegrationConfig } from './environment';
-
-export interface FeatureFlags {
- demoSubmissionEnabled: boolean;
- crmIntegrationEnabled: false;
- schedulerEnabled: false;
- analyticsEnabled: boolean;
- marketingConsentEnabled: false;
- productTourEnabled: false;
- pricingDestinationEnabled: false;
- loginDestinationEnabled: false;
-}
-
-export function buildFeatureFlags(config: PublicIntegrationConfig): FeatureFlags {
- return {
- demoSubmissionEnabled: config.demoSubmissionEnabled,
- crmIntegrationEnabled: false,
- schedulerEnabled: false,
- analyticsEnabled: config.analyticsMode === 'test',
- marketingConsentEnabled: false,
- productTourEnabled: false,
- pricingDestinationEnabled: false,
- loginDestinationEnabled: false,
- };
-}
diff --git a/homepage/src/lib/localization/client-messages.ts b/homepage/src/lib/localization/client-messages.ts
deleted file mode 100644
index 2e81429..0000000
--- a/homepage/src/lib/localization/client-messages.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import arShell from '@/content/locales/ar/shell.json';
-import enShell from '@/content/locales/en/shell.json';
-import frShell from '@/content/locales/fr/shell.json';
-import { isLocale, type Locale } from './config';
-import type { ShellMessages } from './messages';
-
-const shells = {
- en: enShell,
- fr: frShell,
- ar: arShell,
-} satisfies Record;
-
-export function getClientShellMessages(value: unknown): ShellMessages {
- return isLocale(value) ? shells[value] : shells.en;
-}
diff --git a/homepage/src/lib/localization/client.ts b/homepage/src/lib/localization/client.ts
deleted file mode 100644
index 180473d..0000000
--- a/homepage/src/lib/localization/client.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { localeCookie, localeCookieMaxAgeSeconds, localeStorageKey, type Locale } from './config';
-
-export function persistLocale(locale: Locale): void {
- try {
- window.localStorage.setItem(localeStorageKey, locale);
- } catch {
- // The route and first-party cookie remain sufficient when storage is unavailable.
- }
-
- const secure = window.location.protocol === 'https:' ? '; Secure' : '';
- document.cookie =
- [
- `${localeCookie}=${encodeURIComponent(locale)}`,
- 'Path=/',
- `Max-Age=${localeCookieMaxAgeSeconds}`,
- 'SameSite=Lax',
- ].join('; ') + secure;
-}
diff --git a/homepage/src/lib/localization/config.ts b/homepage/src/lib/localization/config.ts
deleted file mode 100644
index 8ff4d2b..0000000
--- a/homepage/src/lib/localization/config.ts
+++ /dev/null
@@ -1,140 +0,0 @@
-import rawManifest from '@/contracts/phase9/route-manifest.json';
-import { z } from 'zod';
-
-export const locales = ['en', 'fr', 'ar'] as const;
-export type Locale = (typeof locales)[number];
-export type Direction = 'ltr' | 'rtl';
-export type RouteId = 'home' | 'sign-in' | 'forgot-password' | 'reset-password' | 'privacy' | 'terms' | 'accessibility';
-
-export const defaultLocale: Locale = 'en';
-export const localeCookie = 'hpc-locale';
-export const localeStorageKey = 'hpc.locale';
-export const localeCookieMaxAgeSeconds = 31_536_000;
-export const directionByLocale: Record = {
- en: 'ltr',
- fr: 'ltr',
- ar: 'rtl',
-};
-
-const localeSchema = z.enum(locales);
-const routeSchema = z.object({
- id: z.enum(['home', 'sign-in', 'forgot-password', 'reset-password', 'privacy', 'terms', 'accessibility']),
- slugs: z.object({ en: z.string(), fr: z.string(), ar: z.string() }),
- indexable: z.boolean(),
- hashes: z.array(z.string()),
-});
-const routeManifestSchema = z.object({
- version: z.string(),
- defaultLocale: localeSchema,
- locales: z.array(localeSchema).length(locales.length),
- rootDetection: z.boolean(),
- routes: z.array(routeSchema),
- switching: z.object({
- preserveQueryAllowlist: z.array(z.string()),
- preserveValidHash: z.boolean(),
- dropUnknownQuery: z.boolean(),
- }),
- hreflang: z.object({
- values: z.array(localeSchema),
- xDefaultLocale: localeSchema,
- selfCanonical: z.boolean(),
- }),
-});
-
-export const routeManifest = routeManifestSchema.parse(rawManifest);
-export const campaignQueryAllowlist = routeManifest.switching.preserveQueryAllowlist;
-
-export function isLocale(value: unknown): value is Locale {
- return typeof value === 'string' && (locales as readonly string[]).includes(value);
-}
-
-export function getDirection(locale: Locale): Direction {
- return directionByLocale[locale];
-}
-
-export function localizedPath(routeId: RouteId, locale: Locale): string {
- const route = routeManifest.routes.find((item) => item.id === routeId);
- if (!route) {
- throw new Error(`Unknown route ID: ${routeId}`);
- }
- const slug = route.slugs[locale];
- return slug.length > 0 ? `/${locale}/${slug}` : `/${locale}`;
-}
-
-export function routeById(routeId: RouteId) {
- const route = routeManifest.routes.find((item) => item.id === routeId);
- if (!route) {
- throw new Error(`Unknown route ID: ${routeId}`);
- }
- return route;
-}
-
-export function localeFromAcceptLanguage(headerValue: string | null): Locale {
- if (!headerValue) return defaultLocale;
-
- const requested = headerValue
- .split(',')
- .map((entry) => {
- const [tag = '', ...parameters] = entry.trim().split(';');
- const qualityParameter = parameters.find((parameter) => parameter.trim().startsWith('q='));
- const quality = qualityParameter ? Number(qualityParameter.trim().slice(2)) : 1;
- return { tag: tag.toLowerCase(), quality: Number.isFinite(quality) ? quality : 0 };
- })
- .sort((a, b) => b.quality - a.quality);
-
- for (const request of requested) {
- const base = request.tag.split('-')[0];
- if (isLocale(base)) return base;
- }
- return defaultLocale;
-}
-
-export function localeSwitchUrl(current: URL, targetLocale: Locale, routeId: RouteId): URL {
- const target = new URL(localizedPath(routeId, targetLocale), current.origin);
- for (const key of campaignQueryAllowlist) {
- const value = current.searchParams.get(key);
- if (value !== null) target.searchParams.set(key, value);
- }
-
- const route = routeById(routeId);
- const hash = current.hash.replace(/^#/, '');
- if (routeManifest.switching.preserveValidHash && route.hashes.includes(hash)) {
- target.hash = hash;
- }
- return target;
-}
-
-export function routeIdFromSlug(locale: Locale, slug: string): RouteId | null {
- let decoded: string;
- try {
- decoded = decodeURIComponent(slug);
- } catch {
- return null;
- }
- const normalized = decoded.replace(/^\/+|\/+$/g, '');
- const route = routeManifest.routes.find((item) => item.slugs[locale] === normalized);
- return route?.id ?? null;
-}
-
-export function routeIdFromPathname(pathname: string): RouteId | null {
- const [localeValue, ...segments] = pathname.split('/').filter(Boolean);
- if (!isLocale(localeValue)) return null;
- if (segments.length === 0) return 'home';
- return routeIdFromSlug(localeValue, segments.join('/'));
-}
-
-export function localizedUrl(origin: URL, routeId: RouteId, locale: Locale): URL {
- return new URL(localizedPath(routeId, locale), origin);
-}
-
-export function hreflangMap(origin: URL, routeId: RouteId): Record {
- const entries = locales.map((locale) => [
- locale,
- new URL(localizedPath(routeId, locale), origin).href,
- ]);
- entries.push([
- 'x-default',
- new URL(localizedPath(routeId, routeManifest.hreflang.xDefaultLocale), origin).href,
- ]);
- return Object.fromEntries(entries);
-}
diff --git a/homepage/src/lib/localization/format.ts b/homepage/src/lib/localization/format.ts
deleted file mode 100644
index 4912bad..0000000
--- a/homepage/src/lib/localization/format.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-import type { Locale } from './config';
-
-const localeTag: Record = {
- en: 'en',
- fr: 'fr',
- ar: 'ar',
-};
-
-export interface DateFormattingContext {
- locale: Locale;
- timeZone: string;
-}
-
-export function formatDate(
- value: Date | number,
- context: DateFormattingContext,
- options: Intl.DateTimeFormatOptions = { dateStyle: 'medium' },
-): string {
- return new Intl.DateTimeFormat(localeTag[context.locale], {
- calendar: 'gregory',
- numberingSystem: 'latn',
- timeZone: context.timeZone,
- ...options,
- }).format(value);
-}
-
-export function formatTime(
- value: Date | number,
- context: DateFormattingContext,
- options: Intl.DateTimeFormatOptions = { timeStyle: 'short' },
-): string {
- return formatDate(value, context, options);
-}
-
-export function formatDateRange(
- start: Date | number,
- end: Date | number,
- context: DateFormattingContext,
- options: Intl.DateTimeFormatOptions = { dateStyle: 'medium' },
-): string {
- const formatter = new Intl.DateTimeFormat(localeTag[context.locale], {
- calendar: 'gregory',
- numberingSystem: 'latn',
- timeZone: context.timeZone,
- ...options,
- });
- return formatter.formatRange(start, end);
-}
-
-export function formatNumber(
- value: number,
- locale: Locale,
- options: Intl.NumberFormatOptions = {},
-): string {
- return new Intl.NumberFormat(localeTag[locale], {
- numberingSystem: 'latn',
- ...options,
- }).format(value);
-}
-
-export function formatPercent(value: number, locale: Locale): string {
- return formatNumber(value, locale, { style: 'percent', maximumFractionDigits: 1 });
-}
-
-export function formatCurrency(value: number, locale: Locale, currency: string): string {
- if (!/^[A-Z]{3}$/.test(currency)) {
- throw new Error('Currency must be an explicit ISO 4217 code.');
- }
- return formatNumber(value, locale, { style: 'currency', currency });
-}
-
-export function formatRelativeTime(
- value: number,
- unit: Intl.RelativeTimeFormatUnit,
- locale: Locale,
-): string {
- return new Intl.RelativeTimeFormat(`${localeTag[locale]}-u-nu-latn`, {
- numeric: 'auto',
- }).format(value, unit);
-}
-
-export function formatPhoneNumber(value: string): string {
- return value.replace(/[^+\d]/g, '').replace(/(?!^)\+/g, '');
-}
-
-export function preserveIdentifier(value: string): string {
- return value.replace(/\s+/g, ' ');
-}
diff --git a/homepage/src/lib/localization/messages.ts b/homepage/src/lib/localization/messages.ts
deleted file mode 100644
index 44e1623..0000000
--- a/homepage/src/lib/localization/messages.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import 'server-only';
-
-import arHomepage from '@/content/locales/ar/homepage.json';
-import arShell from '@/content/locales/ar/shell.json';
-import enHomepage from '@/content/locales/en/homepage.json';
-import enShell from '@/content/locales/en/shell.json';
-import frHomepage from '@/content/locales/fr/homepage.json';
-import frShell from '@/content/locales/fr/shell.json';
-import type { Locale } from './config';
-
-export type HomepageMessages = typeof enHomepage;
-export type ShellMessages = typeof enShell;
-export interface Messages {
- homepage: HomepageMessages;
- shell: ShellMessages;
-}
-
-const messages = {
- en: { homepage: enHomepage, shell: enShell },
- fr: { homepage: frHomepage, shell: frShell },
- ar: { homepage: arHomepage, shell: arShell },
-} satisfies Record;
-
-export function getMessages(locale: Locale): Messages {
- return messages[locale];
-}
diff --git a/homepage/src/lib/localization/site-origin.ts b/homepage/src/lib/localization/site-origin.ts
deleted file mode 100644
index d045d69..0000000
--- a/homepage/src/lib/localization/site-origin.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]']);
-
-export function getSiteOrigin(): URL {
- const configured = process.env.SITE_ORIGIN;
- if (!configured) {
- if (process.env.NODE_ENV !== 'production') return new URL('http://localhost:3000');
- throw new Error('SITE_ORIGIN is required in production. No canonical domain is approved yet.');
- }
-
- const origin = new URL(configured);
- if (origin.pathname !== '/' || origin.search || origin.hash) {
- throw new Error('SITE_ORIGIN must contain only scheme and host.');
- }
- if (
- process.env.NODE_ENV === 'production' &&
- origin.protocol !== 'https:' &&
- !LOOPBACK_HOSTS.has(origin.hostname)
- ) {
- throw new Error('SITE_ORIGIN must use HTTPS in production.');
- }
- return origin;
-}
diff --git a/homepage/src/lib/metadata/build-metadata.ts b/homepage/src/lib/metadata/build-metadata.ts
deleted file mode 100644
index 4dff5e0..0000000
--- a/homepage/src/lib/metadata/build-metadata.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-import {
- hreflangMap,
- localizedPath,
- locales,
- type Locale,
- type RouteId,
-} from '@/lib/localization/config';
-import { getSiteOrigin } from '@/lib/localization/site-origin';
-import type { Metadata } from 'next';
-
-interface LocalizedMetadataInput {
- locale: Locale;
- routeId: RouteId;
- title: string;
- description: string;
- indexable: boolean;
-}
-
-export function buildLocalizedMetadata({
- locale,
- routeId,
- title,
- description,
- indexable,
-}: LocalizedMetadataInput): Metadata {
- const origin = getSiteOrigin();
- const publicReleaseApproved = process.env.PUBLIC_RELEASE_APPROVED === 'true';
- const canIndex = publicReleaseApproved && indexable;
- const canonicalPath = localizedPath(routeId, locale);
-
- return {
- metadataBase: origin,
- title,
- description,
- alternates: {
- canonical: canonicalPath,
- languages: hreflangMap(origin, routeId),
- },
- openGraph: {
- type: 'website',
- siteName: 'RentalDriveGo',
- title,
- description,
- url: canonicalPath,
- locale,
- alternateLocale: locales.filter((candidate) => candidate !== locale),
- },
- twitter: {
- card: 'summary',
- title,
- description,
- },
- robots: canIndex
- ? { index: true, follow: true }
- : { index: false, follow: false, noarchive: true },
- };
-}
diff --git a/homepage/src/lib/security/content-type.ts b/homepage/src/lib/security/content-type.ts
deleted file mode 100644
index fc08ff3..0000000
--- a/homepage/src/lib/security/content-type.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-/**
- * Accept JSON requests with optional media-type parameters while rejecting
- * look-alike types such as application/jsonp or application/json-patch+json.
- */
-export function isApplicationJson(contentType: string | null): boolean {
- if (!contentType) return false;
- const [mediaType] = contentType.split(';', 1);
- return mediaType?.trim().toLowerCase() === 'application/json';
-}
diff --git a/homepage/src/lib/security/csp.ts b/homepage/src/lib/security/csp.ts
deleted file mode 100644
index 7ea0b27..0000000
--- a/homepage/src/lib/security/csp.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-export function createNonce(): string {
- return crypto.randomUUID().replaceAll('-', '');
-}
-
-export function buildContentSecurityPolicy(nonce: string, development: boolean): string {
- const scriptSources = ["'self'", `'nonce-${nonce}'`, "'strict-dynamic'"];
- if (development) scriptSources.push("'unsafe-eval'");
-
- const directives = [
- "default-src 'self'",
- `script-src ${scriptSources.join(' ')}`,
- `style-src 'self'${development ? " 'unsafe-inline'" : ` 'nonce-${nonce}'`}`,
- "img-src 'self' data: blob:",
- "font-src 'self' data:",
- `connect-src 'self'${development ? ' ws: wss:' : ''}`,
- "media-src 'self'",
- "object-src 'none'",
- "base-uri 'self'",
- "form-action 'self'",
- "frame-ancestors 'none'",
- "worker-src 'self' blob:",
- ...(development ? [] : ['upgrade-insecure-requests']),
- ];
- return directives.join('; ');
-}
diff --git a/homepage/src/lib/theme/bootstrap-script.ts b/homepage/src/lib/theme/bootstrap-script.ts
deleted file mode 100644
index 137cf3b..0000000
--- a/homepage/src/lib/theme/bootstrap-script.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-export const themeBootstrapScript = `(function () {
- var allowed = { light: true, dark: true, system: true };
- var cookieMatch = document.cookie.match(/(?:^|; )hpc-theme=([^;]+)/);
- var cookieValue = cookieMatch ? decodeURIComponent(cookieMatch[1]) : null;
- var stored = null;
- try { stored = localStorage.getItem("hpc.theme.preference"); } catch (_) {}
- var preference = allowed[cookieValue] ? cookieValue : (allowed[stored] ? stored : "system");
- var dark = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
- var resolved = preference === "system" ? (dark ? "dark" : "light") : preference;
- var root = document.documentElement;
- root.dataset.themePreference = preference;
- root.dataset.theme = resolved;
-})();`;
diff --git a/homepage/src/lib/theme/client.ts b/homepage/src/lib/theme/client.ts
deleted file mode 100644
index d9eae63..0000000
--- a/homepage/src/lib/theme/client.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import {
- isThemePreference,
- resolveTheme,
- themeCookie,
- themeCookieMaxAgeSeconds,
- themeStorageKey,
- type ThemePreference,
-} from './config';
-
-export const themePreferenceEvent = 'rentaldrivego:theme-preference';
-
-export function applyTheme(preference: ThemePreference): void {
- const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
- const resolved = resolveTheme(preference, systemDark);
- const root = document.documentElement;
- root.dataset.themePreference = preference;
- root.dataset.theme = resolved;
-}
-
-export function persistTheme(preference: ThemePreference): void {
- try {
- window.localStorage.setItem(themeStorageKey, preference);
- } catch {
- // Storage can be unavailable; the first-party cookie remains canonical.
- }
-
- const secure = window.location.protocol === 'https:' ? '; Secure' : '';
- document.cookie =
- [
- `${themeCookie}=${encodeURIComponent(preference)}`,
- 'Path=/',
- `Max-Age=${themeCookieMaxAgeSeconds}`,
- 'SameSite=Lax',
- ].join('; ') + secure;
- applyTheme(preference);
- window.dispatchEvent(
- new CustomEvent(themePreferenceEvent, { detail: preference }),
- );
-}
-
-export function currentThemePreference(): ThemePreference {
- const value = document.documentElement.dataset.themePreference;
- return isThemePreference(value) ? value : 'system';
-}
diff --git a/homepage/src/lib/theme/config.ts b/homepage/src/lib/theme/config.ts
deleted file mode 100644
index 1745de6..0000000
--- a/homepage/src/lib/theme/config.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-export const themeCookie = 'hpc-theme';
-export const themeStorageKey = 'hpc.theme.preference';
-export const themePreferences = ['light', 'dark', 'system'] as const;
-export type ThemePreference = (typeof themePreferences)[number];
-export type ResolvedTheme = 'light' | 'dark';
-
-export const themeCookieMaxAgeSeconds = 31_536_000;
-
-export function isThemePreference(value: unknown): value is ThemePreference {
- return typeof value === 'string' && (themePreferences as readonly string[]).includes(value);
-}
-
-export function resolveTheme(preference: ThemePreference, systemDark: boolean): ResolvedTheme {
- return preference === 'system' ? (systemDark ? 'dark' : 'light') : preference;
-}
-
-export function serverResolvedTheme(preference: ThemePreference): ResolvedTheme {
- return preference === 'dark' ? 'dark' : 'light';
-}
diff --git a/homepage/src/proxy.ts b/homepage/src/proxy.ts
deleted file mode 100644
index bc86433..0000000
--- a/homepage/src/proxy.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-import {
- defaultLocale,
- isLocale,
- localeCookie,
- localeCookieMaxAgeSeconds,
- localeFromAcceptLanguage,
-} from '@/lib/localization/config';
-import { buildContentSecurityPolicy, createNonce } from '@/lib/security/csp';
-import type { NextRequest } from 'next/server';
-import { NextResponse } from 'next/server';
-
-function applySecurityHeaders(response: NextResponse, contentSecurityPolicy: string): NextResponse {
- response.headers.set('Content-Security-Policy', contentSecurityPolicy);
- response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
- response.headers.set('X-Content-Type-Options', 'nosniff');
- response.headers.set('X-Frame-Options', 'DENY');
- response.headers.set('Cross-Origin-Opener-Policy', 'same-origin');
- response.headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
- return response;
-}
-
-export function proxy(request: NextRequest): NextResponse {
- const nonce = createNonce();
- const csp = buildContentSecurityPolicy(nonce, process.env.NODE_ENV !== 'production');
- const requestHeaders = new Headers(request.headers);
- requestHeaders.set('x-nonce', nonce);
- requestHeaders.set('Content-Security-Policy', csp);
-
- if (request.nextUrl.pathname === '/') {
- const cookieLocale = request.cookies.get(localeCookie)?.value;
- const locale = isLocale(cookieLocale)
- ? cookieLocale
- : localeFromAcceptLanguage(request.headers.get('accept-language')) || defaultLocale;
- const destination = request.nextUrl.clone();
- destination.pathname = `/${locale}`;
- const response = NextResponse.redirect(destination);
- response.cookies.set(localeCookie, locale, {
- path: '/',
- sameSite: 'lax',
- maxAge: localeCookieMaxAgeSeconds,
- secure: process.env.NODE_ENV === 'production',
- httpOnly: false,
- });
- return applySecurityHeaders(response, csp);
- }
-
- const response = NextResponse.next({ request: { headers: requestHeaders } });
- const explicitLocale = request.nextUrl.pathname.split('/')[1];
- if (isLocale(explicitLocale)) {
- response.cookies.set(localeCookie, explicitLocale, {
- path: '/',
- sameSite: 'lax',
- maxAge: localeCookieMaxAgeSeconds,
- secure: process.env.NODE_ENV === 'production',
- httpOnly: false,
- });
- }
- return applySecurityHeaders(response, csp);
-}
-
-export const config = {
- matcher: [
- {
- source: '/((?!api|_next/static|_next/image|assets|favicon.ico|robots.txt|sitemap.xml).*)',
- missing: [
- { type: 'header', key: 'next-router-prefetch' },
- { type: 'header', key: 'purpose', value: 'prefetch' },
- ],
- },
- ],
-};
diff --git a/homepage/src/styles/component-tokens.css b/homepage/src/styles/component-tokens.css
deleted file mode 100644
index b70c74b..0000000
--- a/homepage/src/styles/component-tokens.css
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * Phase 11 component-facing token layer.
- * Raw palette values remain in the immutable Phase 9 token source. Components consume only
- * the semantic aliases below. Added roles are documented in docs/TOKEN_IMPLEMENTATION_v1.0.md.
- */
-:root {
- --surface-page: var(--color-canvas);
- --surface-primary: var(--color-surface);
- --surface-elevated: var(--color-surface-raised);
- --surface-muted: var(--color-surface-muted);
- --surface-strong: var(--color-surface-strong);
- --text-primary: var(--color-text);
- --text-secondary: var(--color-text-muted);
- --text-subdued: var(--color-gray-500);
- --text-inverse: var(--color-on-inverse);
- --border-standard: var(--color-border);
- --border-strong: var(--color-border-strong);
- --interactive-primary: var(--color-action-primary);
- --interactive-primary-hover: var(--color-action-primary-hover);
- --interactive-conversion: var(--color-action-conversion);
- --interactive-conversion-hover: var(--color-action-conversion-hover);
- --interactive-danger: var(--color-red-700);
- --link-default: var(--color-action-primary);
- --link-hover: var(--color-action-primary-hover);
- --focus-ring: var(--color-focus);
- --status-success-surface: var(--color-green-100);
- --status-success-text: var(--color-green-700);
- --status-warning-surface: var(--color-amber-100);
- --status-warning-text: var(--color-amber-800);
- --status-error-surface: var(--color-red-100);
- --status-error-text: var(--color-red-700);
- --status-info-surface: var(--color-soft-blue);
- --status-info-text: var(--color-blue-800);
- --overlay-scrim: color-mix(in srgb, var(--color-navy-950) 72%, transparent);
- --control-disabled-surface: var(--color-gray-100);
- --control-disabled-text: var(--color-gray-600);
- --selection-surface: var(--color-blue-200);
- --selection-text: var(--color-gray-900);
- --skeleton-base: var(--color-surface-strong);
- --skeleton-highlight: var(--color-surface-raised);
- --shadow-subtle: 0 1px 2px var(--shadow-color);
- --shadow-card: 0 20px 60px var(--shadow-color);
- --shadow-overlay: 0 28px 80px var(--shadow-color);
- --container-wide: var(--size-container-max);
- --container-content: 960px;
- --container-reading: 720px;
- --layout-gutter: clamp(var(--space-4), 4vw, var(--space-10));
- --section-space: clamp(var(--space-12), 8vw, var(--space-24));
- --header-min-block-size: 76px;
- --header-offset: 92px;
- --layer-base: 0;
- --layer-sticky: 100;
- --layer-overlay: 500;
- --layer-skip-link: 1000;
- --opacity-disabled: 0.62;
- --type-display-size: clamp(2.25rem, 6vw, 4.75rem);
- --type-heading-1-size: clamp(2rem, 5vw, 4rem);
- --type-heading-2-size: clamp(1.75rem, 3vw, 2.75rem);
- --type-heading-3-size: clamp(1.25rem, 2vw, 1.75rem);
- --type-body-large-size: clamp(1.0625rem, 1.5vw, 1.25rem);
- --type-body-size: 1rem;
- --type-label-size: 0.875rem;
- --type-caption-size: 0.8125rem;
- --line-display: 1.05;
- --line-heading: 1.18;
- --line-body: 1.65;
- --line-arabic-body: 1.85;
- --tracking-display: -0.035em;
- --tracking-label: 0.04em;
-}
-
-html[data-theme='dark'] {
- --text-subdued: var(--color-gray-300);
- --status-success-surface: color-mix(in srgb, var(--color-green-700) 24%, var(--color-navy-900));
- --status-success-text: var(--color-green-100);
- --status-warning-surface: color-mix(in srgb, var(--color-amber-800) 30%, var(--color-navy-900));
- --status-warning-text: var(--color-amber-100);
- --status-error-surface: color-mix(in srgb, var(--color-red-700) 28%, var(--color-navy-900));
- --status-error-text: var(--color-red-100);
- --status-info-text: var(--color-blue-100);
- --control-disabled-surface: var(--color-navy-800);
- --control-disabled-text: var(--color-gray-300);
- --selection-surface: var(--color-blue-800);
- --selection-text: var(--color-gray-25);
-}
-
-/* CSS-only fallback for system theme when JavaScript is unavailable or delayed. */
-@media (prefers-color-scheme: dark) {
- html[data-theme-preference='system'] {
- --surface-page: var(--color-navy-950);
- --surface-primary: var(--color-navy-900);
- --surface-elevated: var(--color-navy-850);
- --surface-muted: var(--color-navy-900);
- --surface-strong: var(--color-navy-800);
- --text-primary: var(--color-blue-50);
- --text-secondary: var(--color-gray-300);
- --text-subdued: var(--color-gray-300);
- --text-inverse: var(--color-navy-950);
- --border-standard: var(--color-navy-800);
- --border-strong: var(--color-gray-600);
- --interactive-primary: var(--color-blue-200);
- --interactive-primary-hover: var(--color-blue-100);
- --interactive-conversion: var(--color-orange-400);
- --interactive-conversion-hover: var(--color-orange-100);
- --link-default: var(--color-blue-200);
- --link-hover: var(--color-blue-100);
- --focus-ring: var(--color-orange-400);
- --status-success-surface: color-mix(in srgb, var(--color-green-700) 24%, var(--color-navy-900));
- --status-success-text: var(--color-green-100);
- --status-warning-surface: color-mix(in srgb, var(--color-amber-800) 30%, var(--color-navy-900));
- --status-warning-text: var(--color-amber-100);
- --status-error-surface: color-mix(in srgb, var(--color-red-700) 28%, var(--color-navy-900));
- --status-error-text: var(--color-red-100);
- --status-info-surface: var(--color-navy-800);
- --status-info-text: var(--color-blue-100);
- --control-disabled-surface: var(--color-navy-800);
- --control-disabled-text: var(--color-gray-300);
- --selection-surface: var(--color-blue-800);
- --selection-text: var(--color-gray-25);
- --skeleton-base: var(--color-navy-800);
- --skeleton-highlight: var(--color-navy-850);
- --shadow-subtle: 0 1px 2px color-mix(in srgb, var(--color-navy-950) 70%, transparent);
- --shadow-card: 0 20px 60px color-mix(in srgb, var(--color-navy-950) 70%, transparent);
- --shadow-overlay: 0 28px 80px color-mix(in srgb, var(--color-navy-950) 78%, transparent);
- color-scheme: dark;
- }
-}
diff --git a/homepage/src/styles/globals.css b/homepage/src/styles/globals.css
deleted file mode 100644
index b20121e..0000000
--- a/homepage/src/styles/globals.css
+++ /dev/null
@@ -1,156 +0,0 @@
-* {
- box-sizing: border-box;
-}
-
-html {
- min-inline-size: 320px;
- background: var(--surface-page);
- color: var(--text-primary);
- color-scheme: light;
- scroll-behavior: smooth;
- scroll-padding-block-start: var(--header-offset);
- text-rendering: optimizeLegibility;
-}
-
-html[data-theme='dark'] {
- color-scheme: dark;
-}
-
-body {
- min-block-size: 100dvh;
- margin: 0;
- overflow-wrap: anywhere;
- background: var(--surface-page);
- color: var(--text-primary);
- font-family: var(--font-latin);
- font-size: var(--type-body-size);
- line-height: var(--line-body);
-}
-
-body[data-navigation-open='true'] {
- overflow: hidden;
-}
-
-html[lang='ar'] body {
- font-family: var(--font-arabic);
- line-height: var(--line-arabic-body);
-}
-
-button,
-input,
-select,
-textarea {
- font: inherit;
-}
-
-button,
-select,
-a {
- -webkit-tap-highlight-color: transparent;
-}
-
-img,
-picture,
-svg,
-video {
- display: block;
- max-inline-size: 100%;
-}
-
-h1,
-h2,
-h3,
-p {
- margin-block-start: 0;
-}
-
-h1,
-h2,
-h3 {
- line-height: var(--line-heading);
- text-wrap: balance;
-}
-
-p {
- text-wrap: pretty;
-}
-
-a {
- color: var(--link-default);
- text-underline-offset: 0.18em;
-}
-
-a:hover {
- color: var(--link-hover);
-}
-
-::selection {
- background: var(--selection-surface);
- color: var(--selection-text);
-}
-
-:focus-visible {
- outline: 3px solid var(--focus-ring);
- outline-offset: 3px;
-}
-
-:target {
- scroll-margin-block-start: var(--header-offset);
-}
-
-.skip-link {
- position: fixed;
- z-index: var(--layer-skip-link);
- inset-block-start: var(--space-2);
- inset-inline-start: var(--space-2);
- padding-block: var(--space-3);
- padding-inline: var(--space-4);
- border-radius: var(--radius-sm);
- background: var(--interactive-primary);
- color: var(--text-inverse);
- font-weight: 700;
- text-decoration: none;
- transform: translateY(-160%);
- transition: transform var(--duration-fast) var(--easing-productive);
-}
-
-.skip-link:focus {
- transform: translateY(0);
-}
-
-.visually-hidden {
- position: absolute;
- inline-size: 1px;
- block-size: 1px;
- margin: -1px;
- padding: 0;
- overflow: hidden;
- border: 0;
- clip-path: inset(50%);
- white-space: nowrap;
-}
-
-[aria-disabled='true'] {
- cursor: not-allowed;
-}
-
-bdi,
-[data-bidi-isolate='true'] {
- unicode-bidi: isolate;
-}
-
-@media (forced-colors: active) {
- :focus-visible {
- outline: 3px solid CanvasText;
- }
-
- .skip-link {
- border: 1px solid CanvasText;
- }
-}
-
-@media (prefers-reduced-motion: reduce) {
- html {
- scroll-behavior: auto;
- }
-}
diff --git a/homepage/src/styles/tokens.css b/homepage/src/styles/tokens.css
deleted file mode 100644
index 23e3812..0000000
--- a/homepage/src/styles/tokens.css
+++ /dev/null
@@ -1,130 +0,0 @@
-:root {
- --color-blue-50: #eff6ff;
- --color-blue-100: #dbeafe;
- --color-blue-200: #bfdbfe;
- --color-blue-500: #2874e8;
- --color-blue-600: #1b5dd8;
- --color-blue-700: #174bb5;
- --color-blue-800: #183d88;
- --color-orange-100: #ffedd5;
- --color-orange-400: #fb923c;
- --color-orange-600: #ea580c;
- --color-orange-700: #c2410c;
- --color-navy-950: #081426;
- --color-navy-900: #0d1b2e;
- --color-navy-850: #11233b;
- --color-navy-800: #162b46;
- --color-gray-25: #fbfdff;
- --color-gray-50: #f7f9fc;
- --color-gray-100: #eef2f7;
- --color-gray-200: #dbe3ed;
- --color-gray-300: #c6d0dc;
- --color-gray-500: #64748b;
- --color-gray-600: #475569;
- --color-gray-700: #334155;
- --color-gray-800: #1e293b;
- --color-gray-900: #0f172a;
- --color-green-100: #dcfce7;
- --color-green-700: #15803d;
- --color-amber-100: #fef3c7;
- --color-amber-800: #92400e;
- --color-red-100: #fee2e2;
- --color-red-700: #b91c1c;
- --space-0: 0px;
- --space-1: 4px;
- --space-2: 8px;
- --space-3: 12px;
- --space-4: 16px;
- --space-5: 20px;
- --space-6: 24px;
- --space-7: 28px;
- --space-8: 32px;
- --space-9: 36px;
- --space-10: 40px;
- --space-11: 44px;
- --space-12: 48px;
- --space-13: 52px;
- --space-14: 56px;
- --space-15: 60px;
- --space-16: 64px;
- --space-17: 68px;
- --space-18: 72px;
- --space-19: 76px;
- --space-20: 80px;
- --space-21: 84px;
- --space-22: 88px;
- --space-23: 92px;
- --space-24: 96px;
- --radius-sm: 10px;
- --radius-md: 16px;
- --radius-lg: 24px;
- --radius-xl: 32px;
- --radius-pill: 999px;
- --size-touch-min: 44px;
- --size-container-max: 1280px;
- --duration-fast: 120ms;
- --duration-standard: 160ms;
- --duration-overlay: 220ms;
- --easing-standard: ease;
- --easing-productive: cubic-bezier(0.2, 0, 0, 1);
- --font-latin: Inter Variable, Inter, Arial, sans-serif;
- --font-arabic: Noto Sans Arabic, Arial, sans-serif;
- --breakpoint-medium: 768px;
- --breakpoint-wide: 1120px;
-}
-
-html[data-theme='light'] {
- --color-canvas: var(--color-gray-25);
- --color-surface: #ffffff;
- --color-surface-raised: #ffffff;
- --color-surface-muted: #f2f6fb;
- --color-surface-strong: #e8f0fa;
- --color-text: #102035;
- --color-text-muted: #52647a;
- --color-border: #d8e2ee;
- --color-border-strong: #bdcad9;
- --color-action-primary: var(--color-blue-700);
- --color-action-primary-hover: var(--color-blue-800);
- --color-action-conversion: var(--color-orange-700);
- --color-action-conversion-hover: #9a3412;
- --color-focus: #f97316;
- --color-on-inverse: #f8fafc;
- --color-soft-blue: #eef5ff;
- --color-soft-orange: #fff7ed;
- --shadow-color: rgba(8, 20, 38, 0.12);
- color-scheme: light;
-}
-html[data-theme='dark'] {
- --color-canvas: var(--color-navy-950);
- --color-surface: var(--color-navy-900);
- --color-surface-raised: var(--color-navy-850);
- --color-surface-muted: #0b1a2d;
- --color-surface-strong: #172d48;
- --color-text: #eef5ff;
- --color-text-muted: #adbed2;
- --color-border: #263c56;
- --color-border-strong: #3a536e;
- --color-action-primary: #76a9ff;
- --color-action-primary-hover: #9bc2ff;
- --color-action-conversion: var(--color-orange-400);
- --color-action-conversion-hover: #fdba74;
- --color-focus: var(--color-orange-400);
- --color-on-inverse: var(--color-navy-950);
- --color-soft-blue: #10284a;
- --color-soft-orange: #3a2015;
- --shadow-color: rgba(0, 0, 0, 0.36);
- color-scheme: dark;
-}
-
-@media (prefers-reduced-motion: reduce) {
- html {
- scroll-behavior: auto;
- }
- *,
- *::before,
- *::after {
- animation-duration: 0.01ms !important;
- animation-iteration-count: 1 !important;
- transition-duration: 0.01ms !important;
- }
-}
diff --git a/homepage/tests/a11y/component-system.a11y.spec.ts b/homepage/tests/a11y/component-system.a11y.spec.ts
deleted file mode 100644
index e257d4e..0000000
--- a/homepage/tests/a11y/component-system.a11y.spec.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import AxeBuilder from '@axe-core/playwright';
-import { expect, test } from '@playwright/test';
-
-for (const locale of ['en', 'fr', 'ar'] as const) {
- test(`${locale} component fixture has no detectable accessibility violations`, async ({
- page,
- }) => {
- await page.goto(`/${locale}/component-lab`);
- const results = await new AxeBuilder({ page }).analyze();
- expect(results.violations).toEqual([]);
- });
-}
diff --git a/homepage/tests/a11y/homepage.a11y.spec.ts b/homepage/tests/a11y/homepage.a11y.spec.ts
deleted file mode 100644
index 59503a8..0000000
--- a/homepage/tests/a11y/homepage.a11y.spec.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import AxeBuilder from '@axe-core/playwright';
-import { expect, test } from '@playwright/test';
-
-for (const locale of ['en', 'fr', 'ar'] as const) {
- test(`${locale} homepage has no detectable WCAG A/AA violations`, async ({ page }) => {
- await page.goto(`/${locale}`);
- const results = await new AxeBuilder({ page })
- .withTags(['wcag2a', 'wcag2aa', 'wcag21aa', 'wcag22aa'])
- .analyze();
- expect(results.violations).toEqual([]);
- });
-}
-
-test('homepage keeps a coherent heading outline', async ({ page }) => {
- await page.goto('/en');
- const levels = await page
- .locator('main h1, main h2, main h3')
- .evaluateAll((headings) => headings.map((heading) => Number(heading.tagName.slice(1))));
-
- expect(levels[0]).toBe(1);
- for (let index = 1; index < levels.length; index += 1) {
- expect((levels[index] ?? 1) - (levels[index - 1] ?? 1)).toBeLessThanOrEqual(1);
- }
-});
diff --git a/homepage/tests/a11y/integrations/demo-workflow.a11y.spec.ts b/homepage/tests/a11y/integrations/demo-workflow.a11y.spec.ts
deleted file mode 100644
index 1f1bf68..0000000
--- a/homepage/tests/a11y/integrations/demo-workflow.a11y.spec.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import AxeBuilder from '@axe-core/playwright';
-import { expect, test } from '@playwright/test';
-
-for (const locale of ['en', 'fr', 'ar'] as const) {
- test(`${locale} demo dialog has no detectable WCAG A/AA violations`, async ({ page }) => {
- await page.goto(`/${locale}`);
- await page.locator('main button[data-demo-source="hero"]').click();
- await expect(page.getByRole('dialog')).toBeVisible();
- const results = await new AxeBuilder({ page })
- .include('dialog')
- .withTags(['wcag2a', 'wcag2aa', 'wcag21aa', 'wcag22aa'])
- .analyze();
- expect(results.violations).toEqual([]);
- });
-}
diff --git a/homepage/tests/a11y/shell.a11y.spec.ts b/homepage/tests/a11y/shell.a11y.spec.ts
deleted file mode 100644
index c000c81..0000000
--- a/homepage/tests/a11y/shell.a11y.spec.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import AxeBuilder from '@axe-core/playwright';
-import { expect, test } from '@playwright/test';
-
-for (const locale of ['en', 'fr', 'ar'] as const) {
- for (const theme of ['light', 'dark'] as const) {
- test(`${locale} ${theme} shell has no detectable WCAG A/AA violations`, async ({
- page,
- context,
- }) => {
- await context.addCookies([
- {
- name: 'hpc-theme',
- value: theme,
- domain: '127.0.0.1',
- path: '/',
- secure: false,
- sameSite: 'Lax',
- },
- ]);
- await page.goto(`/${locale}`);
- const results = await new AxeBuilder({ page })
- .withTags(['wcag2a', 'wcag2aa', 'wcag21aa', 'wcag22aa'])
- .analyze();
- expect(results.violations).toEqual([]);
- });
- }
-}
diff --git a/homepage/tests/browser/component-system.spec.ts b/homepage/tests/browser/component-system.spec.ts
deleted file mode 100644
index 7d7a2bb..0000000
--- a/homepage/tests/browser/component-system.spec.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import { expect, test } from '@playwright/test';
-
-for (const locale of ['en', 'fr', 'ar'] as const) {
- test(`${locale} component fixture renders without horizontal overflow`, async ({ page }) => {
- await page.goto(`/${locale}/component-lab`);
- await expect(page.locator('h1')).toBeVisible();
- const overflow = await page.evaluate(
- () => document.documentElement.scrollWidth > document.documentElement.clientWidth,
- );
- expect(overflow).toBe(false);
- await expect(page.locator('html')).toHaveAttribute('dir', locale === 'ar' ? 'rtl' : 'ltr');
- });
-}
-
-test('component controls support keyboard interaction and dialog focus return', async ({
- page,
-}) => {
- await page.goto('/en/component-lab');
- const firstTab = page.getByRole('tab', { name: 'Reservations' });
- await firstTab.focus();
- await page.keyboard.press('ArrowRight');
- await expect(page.getByRole('tab', { name: 'Fleet availability' })).toHaveAttribute(
- 'aria-selected',
- 'true',
- );
-
- const trigger = page.getByRole('button', { name: 'Open dialog' });
- await trigger.click();
- await expect(page.getByRole('dialog')).toBeVisible();
- await page.keyboard.press('Escape');
- await expect(page.getByRole('dialog')).toBeHidden();
- await expect(trigger).toBeFocused();
-});
-
-test('component fixture reflows at narrow mobile and 200 percent zoom equivalent', async ({
- page,
-}) => {
- await page.setViewportSize({ width: 320, height: 800 });
- await page.goto('/fr/component-lab');
- await page.evaluate(() => (document.body.style.zoom = '2'));
- const overflow = await page.evaluate(
- () => document.documentElement.scrollWidth > document.documentElement.clientWidth + 1,
- );
- expect(overflow).toBe(false);
-});
diff --git a/homepage/tests/browser/homepage.spec.ts b/homepage/tests/browser/homepage.spec.ts
deleted file mode 100644
index 7d1d678..0000000
--- a/homepage/tests/browser/homepage.spec.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import { expect, test } from '@playwright/test';
-
-for (const scenario of [
- { locale: 'en', dir: 'ltr' },
- { locale: 'fr', dir: 'ltr' },
- { locale: 'ar', dir: 'rtl' },
-] as const) {
- test(`${scenario.locale} homepage assembles the approved section sequence`, async ({ page }) => {
- await page.goto(`/${scenario.locale}`);
-
- await expect(page.locator('html')).toHaveAttribute('dir', scenario.dir);
- await expect(page.locator('main h1')).toHaveCount(1);
- await expect(page.locator('#product')).toBeVisible();
- await expect(page.locator('#workflow')).toBeVisible();
- await expect(page.locator('#modules')).toBeVisible();
- await expect(page.locator('#pricing')).toBeVisible();
- await expect(page.locator('#faq')).toBeVisible();
- await expect(page.locator('main > section')).toHaveCount(12);
- });
-
- test(`${scenario.locale} unresolved actions remain non-navigable`, async ({ page }) => {
- await page.goto(`/${scenario.locale}`);
-
- const disabledActions = page.locator('main [aria-disabled="true"]');
- await expect(disabledActions).toHaveCount(3);
- await expect(disabledActions.first()).not.toHaveAttribute('href');
- });
-}
-
-test('Arabic preview preserves left-to-right vehicle identifiers', async ({ page }) => {
- await page.goto('/ar');
- const identifiers = page.locator('[role="table"] bdi');
- await expect(identifiers).toHaveCount(2);
- await expect(identifiers.first()).toHaveAttribute('dir', 'ltr');
-});
-
-test('homepage navigation targets resolve to real sections', async ({ page }) => {
- await page.goto('/en');
- for (const hash of ['product', 'workflow', 'modules', 'pricing', 'faq']) {
- await expect(page.locator(`#${hash}`)).toHaveCount(1);
- await expect(page.locator(`header a[href="/en#${hash}"]`)).toHaveCount(2);
- }
-});
-
-test('FAQ uses native disclosure behavior', async ({ page }) => {
- await page.goto('/en');
- const firstItem = page.locator('#faq details').first();
- await expect(firstItem).toHaveAttribute('open', '');
- await firstItem.locator('summary').click();
- await expect(firstItem).not.toHaveAttribute('open', '');
-});
diff --git a/homepage/tests/browser/integrations/demo-workflow.spec.ts b/homepage/tests/browser/integrations/demo-workflow.spec.ts
deleted file mode 100644
index 7913e4e..0000000
--- a/homepage/tests/browser/integrations/demo-workflow.spec.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-import { expect, test } from '@playwright/test';
-
-const localeData = {
- en: {
- button: /Book a demo|Book a product demo/,
- title: 'Book a product demo',
- success: 'Demo request recorded',
- },
- fr: {
- button: /Réserver une démonstration/,
- title: 'Réserver une démonstration du produit',
- success: 'Demande de démonstration enregistrée',
- },
- ar: {
- button: /احجز عرضًا توضيحيًا|احجز عرض/,
- title: 'احجز عرضًا توضيحيًا للمنتج',
- success: 'تم تسجيل طلب العرض',
- },
-} as const;
-
-for (const locale of ['en', 'fr', 'ar'] as const) {
- test(`${locale} demo flow validates and succeeds through the local adapter`, async ({ page }) => {
- const labels = localeData[locale];
- await page.goto(`/${locale}`);
- await page.locator('main button[data-demo-source="hero"]').click();
-
- const dialog = page.getByRole('dialog', { name: labels.title });
- await expect(dialog).toBeVisible();
- await dialog
- .getByRole('button', { name: /Request my demo|Demander ma démonstration|إرسال طلب العرض/ })
- .click();
- await expect(dialog.locator('[role="alert"]')).toBeVisible();
- await expect(dialog.locator('#fullName')).toBeFocused();
-
- await dialog.locator('#fullName').fill('Avery Example');
- await dialog.locator('#workEmail').fill('avery@example.test');
- await dialog.locator('#company').fill('Example Rental Lab');
- await dialog.locator('#fleetSize').selectOption('10-24');
- await dialog
- .getByRole('button', { name: /Request my demo|Demander ma démonstration|إرسال طلب العرض/ })
- .click();
-
- await expect(dialog.getByRole('heading', { name: labels.success })).toBeVisible();
- await expect(page).toHaveURL(new RegExp(`/${locale}$`));
- });
-}
-
-test('duplicate clicks create only one request while submission is pending', async ({ page }) => {
- await page.goto('/en');
- await page.locator('main button[data-demo-source="hero"]').click();
- const dialog = page.getByRole('dialog', { name: 'Book a product demo' });
- await dialog.locator('#fullName').fill('Avery Example');
- await dialog.locator('#workEmail').fill('avery@example.test');
- await dialog.locator('#company').fill('Example Rental Lab');
- await dialog.locator('#fleetSize').selectOption('10-24');
- const submit = dialog.getByRole('button', { name: 'Request my demo' });
- await submit.dblclick();
- await expect(dialog.getByRole('heading', { name: 'Demo request recorded' })).toBeVisible();
-});
-
-test('demo form does not place personal data in the URL or analytics sink', async ({ page }) => {
- await page.goto('/en');
- await page.locator('main button[data-demo-source="hero"]').click();
- const dialog = page.getByRole('dialog', { name: 'Book a product demo' });
- await dialog.locator('#fullName').fill('Private Example');
- await dialog.locator('#workEmail').fill('private@example.test');
- await dialog.locator('#company').fill('Private Rentals');
- await dialog.locator('#fleetSize').selectOption('1-9');
- await dialog.getByRole('button', { name: 'Request my demo' }).click();
- await expect(dialog.getByRole('heading', { name: 'Demo request recorded' })).toBeVisible();
- expect(page.url()).not.toContain('private');
- const analytics = await page.evaluate(() => window.__rdgAnalyticsTestSink ?? []);
- expect(JSON.stringify(analytics)).not.toContain('private');
-});
diff --git a/homepage/tests/browser/navigation.spec.ts b/homepage/tests/browser/navigation.spec.ts
deleted file mode 100644
index fcef635..0000000
--- a/homepage/tests/browser/navigation.spec.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { expect, test } from '@playwright/test';
-
-test.use({ viewport: { width: 390, height: 844 } });
-
-test('mobile navigation opens modally, closes with Escape, and returns focus', async ({ page }) => {
- await page.goto('/en');
- const trigger = page.getByRole('button', { name: 'Open navigation menu' });
- await trigger.focus();
- await trigger.click();
-
- const dialog = page.getByRole('dialog', { name: 'Primary navigation' });
- await expect(dialog).toBeVisible();
- await expect(page.locator('body')).toHaveAttribute('data-navigation-open', 'true');
- await expect(page.getByRole('button', { name: 'Close navigation menu' })).toBeFocused();
-
- await page.keyboard.press('Escape');
- await expect(dialog).not.toBeVisible();
- await expect(trigger).toBeFocused();
-});
-
-test('skip navigation moves focus to the main landmark', async ({ page }) => {
- await page.goto('/en');
- await page.keyboard.press('Tab');
- const skip = page.getByRole('link', { name: 'Skip to main content' });
- await expect(skip).toBeFocused();
- await skip.press('Enter');
- await expect(page.locator('#main-content')).toBeFocused();
-});
diff --git a/homepage/tests/browser/no-javascript.spec.ts b/homepage/tests/browser/no-javascript.spec.ts
deleted file mode 100644
index 407ceff..0000000
--- a/homepage/tests/browser/no-javascript.spec.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { expect, test } from '@playwright/test';
-
-test('server shell remains readable with JavaScript disabled', async ({ browser }) => {
- const context = await browser.newContext({ javaScriptEnabled: false, colorScheme: 'dark' });
- const page = await context.newPage();
- await page.goto('/ar');
- await expect(page.locator('html')).toHaveAttribute('lang', 'ar');
- await expect(page.locator('html')).toHaveAttribute('dir', 'rtl');
- await expect(page.locator('h1')).toBeVisible();
- await expect(page.locator('footer')).toBeVisible();
- await context.close();
-});
diff --git a/homepage/tests/browser/responsive.spec.ts b/homepage/tests/browser/responsive.spec.ts
deleted file mode 100644
index 8fb6151..0000000
--- a/homepage/tests/browser/responsive.spec.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { expect, test } from '@playwright/test';
-
-const widths = [320, 390, 768, 1024, 1440];
-
-for (const locale of ['en', 'fr', 'ar'] as const) {
- for (const width of widths) {
- test(`${locale} has no horizontal overflow at ${width}px`, async ({ page }) => {
- await page.setViewportSize({ width, height: 900 });
- await page.goto(`/${locale}`);
- const overflow = await page.evaluate(
- () => document.documentElement.scrollWidth - document.documentElement.clientWidth,
- );
- expect(overflow).toBeLessThanOrEqual(1);
- });
- }
-}
-
-test('shell remains operable at a 320 CSS pixel reflow equivalent to 200 percent zoom', async ({
- page,
-}) => {
- await page.setViewportSize({ width: 640, height: 800 });
- await page.goto('/fr');
- await expect(page.getByRole('button', { name: 'Ouvrir le menu de navigation' })).toBeVisible();
-});
diff --git a/homepage/tests/browser/shell.spec.ts b/homepage/tests/browser/shell.spec.ts
deleted file mode 100644
index 6dd93d1..0000000
--- a/homepage/tests/browser/shell.spec.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import { expect, test } from '@playwright/test';
-
-for (const scenario of [
- { locale: 'en', dir: 'ltr', title: /RentalDriveGo/ },
- { locale: 'fr', dir: 'ltr', title: /RentalDriveGo/ },
- { locale: 'ar', dir: 'rtl', title: /RentalDriveGo/ },
-] as const) {
- test(`${scenario.locale} shell is server rendered`, async ({ page, request }) => {
- const raw = await request.get(`/${scenario.locale}`);
- const html = await raw.text();
- expect(raw.status()).toBe(200);
- expect(html).toMatch(
- new RegExp(`]*lang=\"${scenario.locale}\"[^>]*dir=\"${scenario.dir}\"`),
- );
-
- const response = await page.goto(`/${scenario.locale}`);
- expect(response?.status()).toBe(200);
- await expect(page.locator('html')).toHaveAttribute('lang', scenario.locale);
- await expect(page.locator('html')).toHaveAttribute('dir', scenario.dir);
- await expect(page).toHaveTitle(scenario.title);
- await expect(page.locator('h1')).toBeVisible();
- await expect(page.locator('header')).toBeVisible();
- await expect(page.locator('footer')).toBeVisible();
- await expect(page.locator('link[rel="canonical"]')).toHaveAttribute(
- 'href',
- new RegExp(`/${scenario.locale}$`),
- );
- for (const language of ['en', 'fr', 'ar', 'x-default']) {
- await expect(page.locator(`link[rel="alternate"][hreflang="${language}"]`)).toHaveCount(1);
- }
- await expect(page.locator('meta[property="og:locale"]')).toHaveAttribute(
- 'content',
- scenario.locale,
- );
- });
-}
-
-test('root locale detection honors Accept-Language', async ({ browser }) => {
- const context = await browser.newContext({
- extraHTTPHeaders: { 'Accept-Language': 'ar;q=1,en;q=0.8' },
- });
- const page = await context.newPage();
- await page.goto('/');
- await expect(page).toHaveURL(/\/ar$/);
- await context.close();
-});
-
-test('locale switching keeps the semantic route and approved campaign state', async ({ page }) => {
- await page.goto('/en/privacy?utm_source=qa&email=drop-me#ignored');
- await page.getByLabel('Language').first().selectOption('fr');
- await expect(page).toHaveURL('/fr/confidentialite?utm_source=qa');
-});
-
-test('pending route shells are localized and noindex', async ({ page }) => {
- await page.goto('/fr/confidentialite');
- await expect(page.locator('h1')).toContainText('Confidentialité');
- await expect(page.locator('meta[name="robots"]')).toHaveAttribute('content', /noindex/);
-});
diff --git a/homepage/tests/browser/text-expansion.spec.ts b/homepage/tests/browser/text-expansion.spec.ts
deleted file mode 100644
index 10e7187..0000000
--- a/homepage/tests/browser/text-expansion.spec.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { expect, test } from '@playwright/test';
-
-for (const locale of ['fr', 'ar'] as const) {
- test(`${locale} shell tolerates long content expansion`, async ({ page }) => {
- await page.setViewportSize({ width: 390, height: 900 });
- await page.goto(`/${locale}`);
- await page
- .locator('main article h3')
- .first()
- .evaluate((element) => {
- element.textContent = [element.textContent, element.textContent, element.textContent]
- .filter(Boolean)
- .join(' ');
- });
- const overflow = await page.evaluate(
- () => document.documentElement.scrollWidth - document.documentElement.clientWidth,
- );
- expect(overflow).toBeLessThanOrEqual(1);
- });
-}
diff --git a/homepage/tests/browser/theme-csp.spec.ts b/homepage/tests/browser/theme-csp.spec.ts
deleted file mode 100644
index fd020f2..0000000
--- a/homepage/tests/browser/theme-csp.spec.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import { expect, test } from '@playwright/test';
-
-test('explicit dark theme is present on the server-rendered root', async ({ browser, baseURL }) => {
- const origin = new URL(baseURL ?? 'http://127.0.0.1:3000');
- const context = await browser.newContext();
- await context.addCookies([
- {
- name: 'hpc-theme',
- value: 'dark',
- domain: origin.hostname,
- path: '/',
- secure: false,
- sameSite: 'Lax',
- },
- ]);
- const page = await context.newPage();
- const response = await page.goto('/en');
- await expect(page.locator('html')).toHaveAttribute('data-theme-preference', 'dark');
- await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');
- const csp = response?.headers()['content-security-policy'] ?? '';
- const nonce = await page.locator('#theme-bootstrap').getAttribute('nonce');
- expect(nonce).toBeTruthy();
- expect(csp).toContain(`'nonce-${nonce}'`);
- await context.close();
-});
-
-test('system preference resolves before interaction', async ({ browser }) => {
- const context = await browser.newContext({ colorScheme: 'dark' });
- const page = await context.newPage();
- await page.goto('/en');
- await expect(page.locator('html')).toHaveAttribute('data-theme-preference', 'system');
- await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');
- await context.close();
-});
-
-test('system follows operating-system changes while explicit preferences ignore them', async ({
- page,
-}) => {
- await page.goto('/en');
- const selector = page.getByLabel('Theme').first();
-
- await selector.selectOption('system');
- await page.emulateMedia({ colorScheme: 'dark' });
- await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');
- await page.emulateMedia({ colorScheme: 'light' });
- await expect(page.locator('html')).toHaveAttribute('data-theme', 'light');
-
- await selector.selectOption('dark');
- await page.emulateMedia({ colorScheme: 'light' });
- await expect(page.locator('html')).toHaveAttribute('data-theme-preference', 'dark');
- await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');
- expect(await page.evaluate(() => window.localStorage.getItem('hpc.theme.preference'))).toBe(
- 'dark',
- );
- expect(await page.context().cookies()).toEqual(
- expect.arrayContaining([expect.objectContaining({ name: 'hpc-theme', value: 'dark' })]),
- );
-});
diff --git a/homepage/tests/fixtures/README.md b/homepage/tests/fixtures/README.md
deleted file mode 100644
index f849acc..0000000
--- a/homepage/tests/fixtures/README.md
+++ /dev/null
@@ -1,5 +0,0 @@
-# Phase 11 foundation fixtures
-
-Phase 10 did not approve a component-workbench dependency. The repository therefore uses the live localized shell plus the matrix in `foundation-matrix.json` as the approved equivalent fixture strategy.
-
-Playwright functional, accessibility, responsive, and visual suites consume the same production routes and components. This avoids a parallel story-only rendering path that could pass while production quietly fails, a trick software is distressingly good at.
diff --git a/homepage/tests/fixtures/component-system/component-matrix.json b/homepage/tests/fixtures/component-system/component-matrix.json
deleted file mode 100644
index c710a0a..0000000
--- a/homepage/tests/fixtures/component-system/component-matrix.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "fixtureRoute": "/{locale}/component-lab",
- "productionGuard": "COMPONENT_FIXTURES_ENABLED=true",
- "locales": ["en", "fr", "ar"],
- "themes": ["light", "dark", "system"],
- "widths": [320, 390, 768, 1024, 1440],
- "states": ["default", "focus-visible", "disabled", "loading", "error", "long-content", "rtl"],
- "contentPolicy": "Fictional deterministic development data only; no production claims, customers, personal data, or live operations."
-}
diff --git a/homepage/tests/fixtures/foundation-matrix.json b/homepage/tests/fixtures/foundation-matrix.json
deleted file mode 100644
index 8f33b68..0000000
--- a/homepage/tests/fixtures/foundation-matrix.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "locales": ["en", "fr", "ar"],
- "directions": ["ltr", "rtl"],
- "themes": ["light", "dark", "system"],
- "viewports": [
- { "name": "mobile", "width": 390, "height": 844 },
- { "name": "tablet", "width": 1024, "height": 900 },
- { "name": "desktop", "width": 1440, "height": 1000 }
- ],
- "shell_states": [
- "default",
- "mobile-navigation-open",
- "keyboard-focus",
- "loading",
- "error",
- "not-found",
- "pending-destination",
- "long-text",
- "reduced-motion"
- ],
- "coverage_source": ["tests/browser", "tests/a11y", "tests/visual"]
-}
diff --git a/homepage/tests/integration/demo-service.test.ts b/homepage/tests/integration/demo-service.test.ts
deleted file mode 100644
index 8f16644..0000000
--- a/homepage/tests/integration/demo-service.test.ts
+++ /dev/null
@@ -1,97 +0,0 @@
-import { clearIdempotencyStoreForTests } from '@/lib/demo/idempotency';
-import type { LeadDestinationAdapter } from '@/lib/demo/adapters';
-import { submitDemoLead } from '@/lib/demo/service';
-import type { DemoLead } from '@/lib/demo/schema';
-import type { IntegrationEnvironment } from '@/lib/integrations/environment';
-import { beforeEach, describe, expect, it, vi } from 'vitest';
-
-const environment: IntegrationEnvironment = {
- NODE_ENV: 'test',
- NEXT_PUBLIC_DEMO_ENABLED: true,
- DEMO_SUBMISSION_MODE: 'local',
- DEMO_REQUEST_TIMEOUT_MS: 250,
- DEMO_IDEMPOTENCY_TTL_SECONDS: 900,
- DEMO_TEST_SCENARIOS_ENABLED: true,
- DEMO_LOCAL_TEST_MODE: false,
- NEXT_PUBLIC_ANALYTICS_MODE: 'disabled',
-};
-
-const lead: DemoLead = {
- fullName: 'Avery Example',
- workEmail: 'avery@example.test',
- company: 'Example Rental Lab',
- fleetSize: '10-24',
- preferredLanguage: 'en',
- idempotencyKey: '6b4992d5-d88e-4e44-9b66-adc18fd8ee37',
- source: 'hero',
-};
-
-beforeEach(clearIdempotencyStoreForTests);
-
-describe('demo submission orchestration', () => {
- it('confirms acceptance before returning success', async () => {
- const adapter: LeadDestinationAdapter = {
- name: 'test-success',
- submitLead: vi.fn(async () => ({
- ok: true as const,
- externalId: 'local_test',
- acceptedAt: '2026-06-25T00:00:00.000Z',
- mode: 'local' as const,
- })),
- };
- const result = await submitDemoLead(lead, { environment, adapter });
- expect(result.ok).toBe(true);
- expect(adapter.submitLead).toHaveBeenCalledOnce();
- });
-
- it('coalesces repeated transport with the same idempotency key', async () => {
- const adapter: LeadDestinationAdapter = {
- name: 'test-idempotent',
- submitLead: vi.fn(async () => ({
- ok: true as const,
- externalId: 'local_once',
- acceptedAt: '2026-06-25T00:00:00.000Z',
- mode: 'local' as const,
- })),
- };
- const [first, second] = await Promise.all([
- submitDemoLead(lead, { environment, adapter }),
- submitDemoLead(lead, { environment, adapter }),
- ]);
- expect(first).toEqual(second);
- expect(adapter.submitLead).toHaveBeenCalledOnce();
- });
-
- it('normalizes adapter failures without exposing vendor bodies', async () => {
- const adapter: LeadDestinationAdapter = {
- name: 'test-error',
- submitLead: async () => ({
- ok: false,
- category: 'integration',
- retryable: true,
- code: 'normalized_failure',
- }),
- };
- const result = await submitDemoLead(lead, { environment, adapter });
- expect(result).toMatchObject({
- ok: false,
- category: 'integration',
- formErrorCode: 'normalized_failure',
- retryable: true,
- });
- });
-
- it('bounds a hung integration with a timeout', async () => {
- const adapter: LeadDestinationAdapter = {
- name: 'test-timeout',
- submitLead: (_lead, context) =>
- new Promise((_resolve, reject) => {
- context.signal.addEventListener('abort', () =>
- reject(new DOMException('Aborted', 'AbortError')),
- );
- }),
- };
- const result = await submitDemoLead(lead, { environment, adapter });
- expect(result).toMatchObject({ ok: false, category: 'timeout', retryable: true });
- });
-});
diff --git a/homepage/tests/setup.ts b/homepage/tests/setup.ts
deleted file mode 100644
index bb02c60..0000000
--- a/homepage/tests/setup.ts
+++ /dev/null
@@ -1 +0,0 @@
-import '@testing-library/jest-dom/vitest';
diff --git a/homepage/tests/types/component-contracts.test-d.tsx b/homepage/tests/types/component-contracts.test-d.tsx
deleted file mode 100644
index 5d6fa4a..0000000
--- a/homepage/tests/types/component-contracts.test-d.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import { Button } from '@/components/actions/Button';
-import type { MetricContent } from '@/components/marketing/Evidence';
-
-void Button;
-
-const approvedMetric: MetricContent = {
- value: '1',
- label: 'Fixture',
- status: 'approved',
- sourceReference: 'SRC-001',
-};
-void approvedMetric;
-
-// @ts-expect-error icon-only buttons require an accessible label.
-const invalidIconButton = ;
-void invalidIconButton;
diff --git a/homepage/tests/unit/components/actions.test.tsx b/homepage/tests/unit/components/actions.test.tsx
deleted file mode 100644
index 60f2129..0000000
--- a/homepage/tests/unit/components/actions.test.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { Button } from '@/components/actions/Button';
-import { ActionLink } from '@/components/actions/ActionLink';
-import { fireEvent, render, screen } from '@testing-library/react';
-import { describe, expect, it, vi } from 'vitest';
-
-describe('action components', () => {
- it('prevents duplicate interaction while loading and preserves the accessible name', () => {
- const onClick = vi.fn();
- render(
- ,
- );
- const button = screen.getByRole('button', { name: 'Save reservation' });
- expect(button).toBeDisabled();
- expect(button).toHaveAttribute('aria-busy', 'true');
- fireEvent.click(button);
- expect(onClick).not.toHaveBeenCalled();
- });
-
- it('requires an accessible label for icon-only use through its public contract', () => {
- render();
- expect(screen.getByRole('button', { name: 'Close panel' })).toBeVisible();
- });
-
- it('renders unresolved navigation as a disabled non-link', () => {
- render(
-
- Book a demo
- ,
- );
- expect(screen.queryByRole('link')).not.toBeInTheDocument();
- expect(screen.getByText('Book a demo').closest('[aria-disabled="true"]')).toBeInTheDocument();
- });
-});
diff --git a/homepage/tests/unit/components/controls.test.tsx b/homepage/tests/unit/components/controls.test.tsx
deleted file mode 100644
index bc9434d..0000000
--- a/homepage/tests/unit/components/controls.test.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import { Accordion } from '@/components/controls/Accordion';
-import { Tabs } from '@/components/controls/Tabs';
-import { render, screen } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import { describe, expect, it } from 'vitest';
-
-describe('selection and disclosure controls', () => {
- it('moves tab focus and selection with arrow keys', async () => {
- const user = userEvent.setup();
- render(
- ,
- );
- const first = screen.getByRole('tab', { name: 'One' });
- await user.click(first);
- await user.keyboard('{ArrowRight}');
- expect(screen.getByRole('tab', { name: 'Two' })).toHaveAttribute('aria-selected', 'true');
- expect(screen.getByRole('tabpanel', { name: 'Two' })).toHaveTextContent('Panel two');
- });
-
- it('uses native disclosure semantics for accordion items', () => {
- render();
- expect(screen.getByText('Question').closest('summary')).toBeInTheDocument();
- expect(screen.getByText('Answer').closest('details')).not.toHaveAttribute('open');
- });
-});
diff --git a/homepage/tests/unit/components/evidence.test.tsx b/homepage/tests/unit/components/evidence.test.tsx
deleted file mode 100644
index 4b240a9..0000000
--- a/homepage/tests/unit/components/evidence.test.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import { canRenderProductionEvidence, Metric } from '@/components/marketing/Evidence';
-import { render, screen } from '@testing-library/react';
-import { describe, expect, it } from 'vitest';
-
-describe('evidence governance', () => {
- it('allows only approved evidence in production mode', () => {
- expect(canRenderProductionEvidence('approved')).toBe(true);
- expect(canRenderProductionEvidence('pending-review')).toBe(false);
- expect(canRenderProductionEvidence('research-only')).toBe(false);
- expect(canRenderProductionEvidence('prohibited')).toBe(false);
- });
-
- it('suppresses research-only metrics in production mode', () => {
- const { container } = render(
- ,
- );
- expect(container).toBeEmptyDOMElement();
- });
-
- it('renders approved evidence with its source reference', () => {
- render(
- ,
- );
- expect(screen.getByText('SRC-001')).toBeVisible();
- });
-});
diff --git a/homepage/tests/unit/components/forms.test.tsx b/homepage/tests/unit/components/forms.test.tsx
deleted file mode 100644
index 6c17105..0000000
--- a/homepage/tests/unit/components/forms.test.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import { ErrorSummary } from '@/components/forms/ErrorSummary';
-import { FormField } from '@/components/forms/FormField';
-import { TextInput } from '@/components/forms/TextInput';
-import { render, screen } from '@testing-library/react';
-import { describe, expect, it } from 'vitest';
-
-describe('form primitives', () => {
- it('associates labels, help, required state, and errors', () => {
- render(
-
-
- ,
- );
- const input = screen.getByLabelText(/Work email/);
- expect(input).toHaveAttribute('required');
- expect(input).toHaveAttribute('aria-invalid', 'true');
- expect(input).toHaveAccessibleDescription('Use work email Enter a valid address');
- });
-
- it('links summary errors back to fields', () => {
- render(
- ,
- );
- expect(screen.getByRole('alert').querySelector('a')).toHaveAttribute('href', '#work-email');
- });
-});
diff --git a/homepage/tests/unit/csp.test.ts b/homepage/tests/unit/csp.test.ts
deleted file mode 100644
index c4df084..0000000
--- a/homepage/tests/unit/csp.test.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { buildContentSecurityPolicy, createNonce } from '@/lib/security/csp';
-import { describe, expect, it } from 'vitest';
-
-describe('CSP foundation', () => {
- it('generates unpredictable nonce-shaped values', () => {
- const first = createNonce();
- const second = createNonce();
- expect(first).toMatch(/^[a-f0-9]{32}$/);
- expect(second).not.toBe(first);
- });
-
- it('allows development evaluation only in development', () => {
- expect(buildContentSecurityPolicy('abc', true)).toContain("'unsafe-eval'");
- expect(buildContentSecurityPolicy('abc', false)).not.toContain("'unsafe-eval'");
- expect(buildContentSecurityPolicy('abc', false)).toContain("'nonce-abc'");
- expect(buildContentSecurityPolicy('abc', false)).toContain("style-src 'self' 'nonce-abc'");
- expect(buildContentSecurityPolicy('abc', false)).not.toContain("'unsafe-inline'");
- expect(buildContentSecurityPolicy('abc', false)).toContain("frame-ancestors 'none'");
- });
-});
diff --git a/homepage/tests/unit/format.test.ts b/homepage/tests/unit/format.test.ts
deleted file mode 100644
index 6f15575..0000000
--- a/homepage/tests/unit/format.test.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import {
- formatCurrency,
- formatDate,
- formatDateRange,
- formatNumber,
- formatPercent,
- formatPhoneNumber,
- formatRelativeTime,
- preserveIdentifier,
-} from '@/lib/localization/format';
-import { describe, expect, it } from 'vitest';
-
-const instant = new Date('2026-06-25T12:00:00.000Z');
-
-describe('locale-aware formatting primitives', () => {
- it('formats Gregorian dates with explicit time zones', () => {
- expect(formatDate(instant, { locale: 'en', timeZone: 'UTC' })).toContain('2026');
- expect(formatDate(instant, { locale: 'fr', timeZone: 'UTC' })).toContain('2026');
- expect(formatDate(instant, { locale: 'ar', timeZone: 'UTC' })).toContain('2026');
- });
-
- it('formats ranges, numbers, percentages, and explicit currencies', () => {
- expect(
- formatDateRange(instant, new Date('2026-06-27T12:00:00.000Z'), {
- locale: 'en',
- timeZone: 'UTC',
- }),
- ).toContain('2026');
- expect(formatNumber(1234.5, 'fr')).toMatch(/1.*234/);
- expect(formatPercent(0.125, 'en')).toContain('12.5');
- expect(formatCurrency(125, 'en', 'USD')).toContain('$');
- expect(() => formatCurrency(125, 'en', 'usd')).toThrow(/ISO 4217/);
- });
-
- it('formats relative time and preserves atomic mixed-direction values', () => {
- expect(formatRelativeTime(-1, 'day', 'en')).toBe('yesterday');
- expect(formatPhoneNumber('+1 (202) 555-0100')).toBe('+12025550100');
- expect(preserveIdentifier('RES 2026 0042')).toBe('RES 2026 0042');
- });
-});
diff --git a/homepage/tests/unit/homepage-content.test.ts b/homepage/tests/unit/homepage-content.test.ts
deleted file mode 100644
index 6ba23e5..0000000
--- a/homepage/tests/unit/homepage-content.test.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import arHomepage from '@/content/locales/ar/homepage.json';
-import arShell from '@/content/locales/ar/shell.json';
-import enHomepage from '@/content/locales/en/homepage.json';
-import enShell from '@/content/locales/en/shell.json';
-import frHomepage from '@/content/locales/fr/homepage.json';
-import frShell from '@/content/locales/fr/shell.json';
-import { buildHomepageContent } from '@/content/homepage-model';
-import { describe, expect, it } from 'vitest';
-
-describe('homepage content model', () => {
- it.each([
- ['en', enHomepage, enShell],
- ['fr', frHomepage, frShell],
- ['ar', arHomepage, arShell],
- ] as const)('builds complete %s route content', (_locale, homepage, shell) => {
- const content = buildHomepageContent(homepage, shell);
-
- expect(content.hero.title).toBeTruthy();
- expect(content.trust.items).toHaveLength(4);
- expect(content.workflow.steps).toHaveLength(4);
- expect(content.modules.items).toHaveLength(6);
- expect(content.faq.items).toHaveLength(6);
- expect(content.hero.preview.rows).toHaveLength(2);
- });
-
- it('keeps unresolved conversion destinations disabled', () => {
- const content = buildHomepageContent(enHomepage, enShell);
-
- expect(content.hero.primary.disabledReason).toBe(enShell.header.demoUnavailable);
- expect(content.pricing.action.disabledReason).toBe(enShell.states.pendingBody);
- expect(content.final.secondary.disabledReason).toBe(enShell.states.pendingBody);
- });
-
- it('keeps preview identifiers isolated from translated prose', () => {
- const content = buildHomepageContent(arHomepage, arShell);
-
- expect(content.hero.preview.rows.map((row) => row.secondary)).toEqual([
- arHomepage.preview.plate1,
- arHomepage.preview.plate2,
- ]);
- });
-});
diff --git a/homepage/tests/unit/integrations/analytics.test.ts b/homepage/tests/unit/integrations/analytics.test.ts
deleted file mode 100644
index c575084..0000000
--- a/homepage/tests/unit/integrations/analytics.test.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { analyticsEventRegistry, validateAnalyticsEvent } from '@/lib/analytics/events';
-import { describe, expect, it } from 'vitest';
-
-const context = {
- routeId: 'home',
- locale: 'en',
- direction: 'ltr',
- resolvedTheme: 'light',
- source: 'hero',
-} as const;
-
-describe('analytics contract', () => {
- it('accepts allowlisted non-personal demo events', () => {
- expect(validateAnalyticsEvent('demo_open', context)).toEqual({ name: 'demo_open', context });
- });
-
- it('rejects unknown events and payload properties', () => {
- expect(validateAnalyticsEvent('lead_created', context)).toBeNull();
- expect(
- validateAnalyticsEvent('demo_open', { ...context, email: 'person@example.test' }),
- ).toBeNull();
- });
-
- it('marks every registry entry as non-personal and consent-gated', () => {
- expect(analyticsEventRegistry.every((event) => !event.personalData)).toBe(true);
- expect(analyticsEventRegistry.every((event) => event.consentCategory === 'analytics')).toBe(
- true,
- );
- });
-});
diff --git a/homepage/tests/unit/integrations/content-type.test.ts b/homepage/tests/unit/integrations/content-type.test.ts
deleted file mode 100644
index 897501a..0000000
--- a/homepage/tests/unit/integrations/content-type.test.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { isApplicationJson } from '@/lib/security/content-type';
-import { describe, expect, it } from 'vitest';
-
-describe('demo API content type validation', () => {
- it.each([
- 'application/json',
- 'application/json; charset=utf-8',
- ' Application/JSON ; charset=UTF-8',
- ])('accepts %s', (value) => {
- expect(isApplicationJson(value)).toBe(true);
- });
-
- it.each([null, '', 'text/plain', 'application/jsonp', 'application/json-patch+json'])(
- 'rejects %s',
- (value) => {
- expect(isApplicationJson(value)).toBe(false);
- },
- );
-});
diff --git a/homepage/tests/unit/integrations/demo-schema.test.ts b/homepage/tests/unit/integrations/demo-schema.test.ts
deleted file mode 100644
index 9bbd017..0000000
--- a/homepage/tests/unit/integrations/demo-schema.test.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import { demoLeadSchema, demoSubmissionEnvelopeSchema, fleetSizeValues } from '@/lib/demo/schema';
-import { describe, expect, it } from 'vitest';
-
-const validLead = {
- fullName: ' Ada Example ',
- workEmail: ' ADA@EXAMPLE.TEST ',
- company: ' Rental Test Labs ',
- fleetSize: '25-49',
- market: '',
- preferredLanguage: 'en',
- message: ' Reservation workflow ',
- idempotencyKey: '154249a8-0c3c-4ad8-bda6-91b1ffb361a8',
- source: 'hero',
-} as const;
-
-describe('demo lead schema', () => {
- it('normalizes approved fields without adding sales-enrichment data', () => {
- const result = demoLeadSchema.parse(validLead);
- expect(result).toEqual({
- ...validLead,
- fullName: 'Ada Example',
- workEmail: 'ada@example.test',
- company: 'Rental Test Labs',
- market: undefined,
- message: 'Reservation workflow',
- });
- });
-
- it('rejects unknown fields and unsupported fleet sizes', () => {
- expect(() => demoLeadSchema.parse({ ...validLead, phone: '+1 555 0100' })).toThrow();
- expect(() => demoLeadSchema.parse({ ...validLead, fleetSize: '5000+' })).toThrow();
- });
-
- it('keeps the Phase 9 fleet ranges stable', () => {
- expect(fleetSizeValues).toEqual(['1-9', '10-24', '25-49', '50-99', '100-249', '250+']);
- });
-
- it('rejects transport fields outside the approved envelope', () => {
- expect(() =>
- demoSubmissionEnvelopeSchema.parse({
- lead: validLead,
- guard: { honeypot: '', startedAtMs: 1, fingerprint: 'nope' },
- }),
- ).toThrow();
- });
-});
diff --git a/homepage/tests/unit/integrations/destinations.test.ts b/homepage/tests/unit/integrations/destinations.test.ts
deleted file mode 100644
index 6439d1e..0000000
--- a/homepage/tests/unit/integrations/destinations.test.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { destinationRegistry, resolveDestination } from '@/lib/integrations/destinations';
-import { describe, expect, it } from 'vitest';
-
-describe('destination registry', () => {
- it('activates only the safe local demo destination', () => {
- const registry = destinationRegistry({ demoSubmissionEnabled: true, demoMode: 'local' });
- expect(registry['book-demo'].status).toBe('active');
- expect(registry['contact-sales'].status).toBe('blocked');
- expect(registry['sign-in'].status).toBe('active');
- expect(registry.pricing.status).toBe('placeholder');
- });
-
- it('fails the demo destination closed when integration is disabled', () => {
- const registry = destinationRegistry({ demoSubmissionEnabled: false, demoMode: 'blocked' });
- expect(registry['book-demo'].status).toBe('blocked');
- });
-
- it('resolves approved localized placeholder routes without guessing external URLs', () => {
- expect(
- resolveDestination('privacy', 'fr', { demoSubmissionEnabled: false, demoMode: 'blocked' })
- .href,
- ).toBe('/fr/confidentialite');
- expect(
- resolveDestination('sign-in', 'en', { demoSubmissionEnabled: false, demoMode: 'blocked' }).href,
- ).toBe('/en/sign-in');
- });
-});
diff --git a/homepage/tests/unit/integrations/redaction.test.ts b/homepage/tests/unit/integrations/redaction.test.ts
deleted file mode 100644
index 543406f..0000000
--- a/homepage/tests/unit/integrations/redaction.test.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { containsSensitiveKey, redactSensitive } from '@/lib/demo/redaction';
-import { describe, expect, it } from 'vitest';
-
-describe('privacy-safe diagnostics', () => {
- it('redacts nested personal fields', () => {
- expect(
- redactSensitive({
- category: 'validation',
- lead: { workEmail: 'person@example.test', company: 'Example Rentals' },
- }),
- ).toEqual({
- category: 'validation',
- lead: { workEmail: '[REDACTED]', company: '[REDACTED]' },
- });
- });
-
- it('detects sensitive diagnostic structures', () => {
- expect(containsSensitiveKey({ context: { message: 'private' } })).toBe(true);
- expect(containsSensitiveKey({ category: 'timeout', correlationId: 'abc' })).toBe(false);
- });
-});
diff --git a/homepage/tests/unit/localization.test.ts b/homepage/tests/unit/localization.test.ts
deleted file mode 100644
index c5d3b8f..0000000
--- a/homepage/tests/unit/localization.test.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import {
- getDirection,
- hreflangMap,
- isLocale,
- localeFromAcceptLanguage,
- localeSwitchUrl,
- localizedPath,
- routeById,
- routeIdFromPathname,
- routeIdFromSlug,
-} from '@/lib/localization/config';
-import { describe, expect, it } from 'vitest';
-
-describe('localization foundations', () => {
- it('selects a supported locale by quality', () => {
- expect(localeFromAcceptLanguage('de-DE;q=0.9, ar;q=0.8, fr;q=0.7')).toBe('ar');
- expect(localeFromAcceptLanguage('fr-CA, en;q=0.8')).toBe('fr');
- expect(localeFromAcceptLanguage(null)).toBe('en');
- });
-
- it('validates locale direction and route lookup', () => {
- expect(isLocale('fr')).toBe(true);
- expect(isLocale('de')).toBe(false);
- expect(getDirection('ar')).toBe('rtl');
- expect(routeById('home').id).toBe('home');
- });
-
- it('generates and resolves localized stable route paths', () => {
- expect(localizedPath('home', 'ar')).toBe('/ar');
- expect(localizedPath('privacy', 'fr')).toBe('/fr/confidentialite');
- expect(localizedPath('sign-in', 'ar')).toBe('/ar/تسجيل-الدخول');
- expect(routeIdFromSlug('fr', 'confidentialite')).toBe('privacy');
- expect(routeIdFromPathname('/ar/%D8%A7%D9%84%D8%AE%D8%B5%D9%88%D8%B5%D9%8A%D8%A9')).toBe(
- 'privacy',
- );
- expect(routeIdFromPathname('/de')).toBeNull();
- });
-
- it('generates reciprocal hreflang values', () => {
- const links = hreflangMap(new URL('https://approved.test'), 'home');
- expect(links.en).toBe('https://approved.test/en');
- expect(links.ar).toBe('https://approved.test/ar');
- expect(links['x-default']).toBe('https://approved.test/en');
- });
-
- it('preserves only approved campaign query values and valid hashes', () => {
- const current = new URL(
- 'https://approved.test/en?utm_source=campaign&email=private%40approved.test#gibberish',
- );
- const switched = localeSwitchUrl(current, 'fr', 'home');
- expect(switched.pathname).toBe('/fr');
- expect(switched.search).toBe('?utm_source=campaign');
- expect(switched.hash).toBe('');
-
- current.hash = '#workflow';
- expect(localeSwitchUrl(current, 'ar', 'home').hash).toBe('#workflow');
- });
-});
diff --git a/homepage/tests/unit/theme.test.ts b/homepage/tests/unit/theme.test.ts
deleted file mode 100644
index c4f374f..0000000
--- a/homepage/tests/unit/theme.test.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { isThemePreference, resolveTheme, serverResolvedTheme } from '@/lib/theme/config';
-import { describe, expect, it } from 'vitest';
-
-describe('theme foundations', () => {
- it('accepts only approved preferences', () => {
- expect(isThemePreference('light')).toBe(true);
- expect(isThemePreference('dark')).toBe(true);
- expect(isThemePreference('system')).toBe(true);
- expect(isThemePreference('sepia')).toBe(false);
- });
-
- it('keeps preference and resolved theme separate', () => {
- expect(resolveTheme('system', true)).toBe('dark');
- expect(resolveTheme('system', false)).toBe('light');
- expect(resolveTheme('light', true)).toBe('light');
- expect(serverResolvedTheme('system')).toBe('light');
- });
-});
diff --git a/homepage/tests/visual/README.md b/homepage/tests/visual/README.md
deleted file mode 100644
index f49a8c0..0000000
--- a/homepage/tests/visual/README.md
+++ /dev/null
@@ -1,10 +0,0 @@
-# Visual baselines
-
-The Playwright scenarios define the Phase 11 locale, theme, viewport, drawer, and focus-state matrix.
-Canonical PNG snapshots must be generated with the pinned Chromium build in an unblocked environment:
-
-```bash
-pnpm test:visual:update --project=chromium
-```
-
-No fabricated or manually composed screenshots are stored here. The local execution environment blocked browser installation and loopback browser navigation, so baseline approval remains an explicit release gate in the Phase 11 issue register.
diff --git a/homepage/tests/visual/component-system.visual.spec.ts b/homepage/tests/visual/component-system.visual.spec.ts
deleted file mode 100644
index cff4953..0000000
--- a/homepage/tests/visual/component-system.visual.spec.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { expect, test } from '@playwright/test';
-
-for (const scenario of [
- { locale: 'en', theme: 'light', width: 1440, height: 1200 },
- { locale: 'fr', theme: 'dark', width: 1024, height: 1200 },
- { locale: 'ar', theme: 'dark', width: 390, height: 1200 },
-] as const) {
- test(`${scenario.locale}-${scenario.theme} component matrix`, async ({ page }) => {
- test.skip(test.info().project.name !== 'chromium', 'Canonical visual baselines use Chromium.');
- await page.addInitScript(
- (theme) => localStorage.setItem('hpc.theme.preference', theme),
- scenario.theme,
- );
- await page.setViewportSize({ width: scenario.width, height: scenario.height });
- await page.goto(`/${scenario.locale}/component-lab`);
- await expect(page).toHaveScreenshot(
- `component-system-${scenario.locale}-${scenario.theme}-${scenario.width}.png`,
- { fullPage: true },
- );
- });
-}
diff --git a/homepage/tests/visual/homepage.visual.spec.ts b/homepage/tests/visual/homepage.visual.spec.ts
deleted file mode 100644
index ac7a0b9..0000000
--- a/homepage/tests/visual/homepage.visual.spec.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { expect, test } from '@playwright/test';
-
-const scenarios = [
- { name: 'homepage-en-light-desktop', locale: 'en', theme: 'light', width: 1440, height: 1000 },
- { name: 'homepage-en-dark-mobile', locale: 'en', theme: 'dark', width: 390, height: 844 },
- { name: 'homepage-fr-light-tablet', locale: 'fr', theme: 'light', width: 1024, height: 900 },
- { name: 'homepage-fr-dark-mobile', locale: 'fr', theme: 'dark', width: 390, height: 844 },
- { name: 'homepage-ar-light-desktop', locale: 'ar', theme: 'light', width: 1440, height: 1000 },
- { name: 'homepage-ar-dark-mobile', locale: 'ar', theme: 'dark', width: 390, height: 844 },
- { name: 'homepage-en-light-320', locale: 'en', theme: 'light', width: 320, height: 800 },
-] as const;
-
-for (const scenario of scenarios) {
- test(`${scenario.name} baseline`, async ({ page, context }) => {
- test.skip(test.info().project.name !== 'chromium');
- await page.setViewportSize({ width: scenario.width, height: scenario.height });
- await context.addCookies([
- {
- name: 'hpc-theme',
- value: scenario.theme,
- domain: '127.0.0.1',
- path: '/',
- secure: false,
- sameSite: 'Lax',
- },
- ]);
- await page.goto(`/${scenario.locale}`);
- await expect(page).toHaveScreenshot(`${scenario.name}.png`, { fullPage: true });
- });
-}
diff --git a/homepage/tests/visual/integrations.visual.spec.ts b/homepage/tests/visual/integrations.visual.spec.ts
deleted file mode 100644
index 6f2f39a..0000000
--- a/homepage/tests/visual/integrations.visual.spec.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { expect, test } from '@playwright/test';
-
-for (const scenario of [
- { locale: 'en', theme: 'light' },
- { locale: 'en', theme: 'dark' },
- { locale: 'fr', theme: 'light' },
- { locale: 'ar', theme: 'light' },
- { locale: 'ar', theme: 'dark' },
-] as const) {
- test(`${scenario.locale} ${scenario.theme} demo initial and validation states`, async ({
- page,
- }) => {
- test.skip(test.info().project.name !== 'chromium', 'Canonical visual baselines use Chromium.');
- await page.emulateMedia({ colorScheme: scenario.theme });
- await page.goto(`/${scenario.locale}`);
- await page.locator('main button[data-demo-source="hero"]').click();
- const dialog = page.getByRole('dialog');
- await expect(dialog).toHaveScreenshot(`demo-${scenario.locale}-${scenario.theme}-initial.png`);
- await dialog.locator('button[type="submit"]').click();
- await expect(dialog).toHaveScreenshot(
- `demo-${scenario.locale}-${scenario.theme}-validation.png`,
- );
- });
-}
diff --git a/homepage/tests/visual/shell.visual.spec.ts b/homepage/tests/visual/shell.visual.spec.ts
deleted file mode 100644
index c01e530..0000000
--- a/homepage/tests/visual/shell.visual.spec.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import { expect, test } from '@playwright/test';
-
-const scenarios = [
- {
- name: 'desktop-en-light',
- locale: 'en',
- theme: 'light',
- viewport: { width: 1440, height: 1000 },
- },
- { name: 'desktop-en-dark', locale: 'en', theme: 'dark', viewport: { width: 1440, height: 1000 } },
- { name: 'tablet-fr-light', locale: 'fr', theme: 'light', viewport: { width: 1024, height: 900 } },
- { name: 'tablet-fr-dark', locale: 'fr', theme: 'dark', viewport: { width: 1024, height: 900 } },
- { name: 'mobile-ar-light', locale: 'ar', theme: 'light', viewport: { width: 390, height: 844 } },
- { name: 'mobile-ar-dark', locale: 'ar', theme: 'dark', viewport: { width: 390, height: 844 } },
-] as const;
-
-for (const scenario of scenarios) {
- test(`${scenario.name} visual baseline`, async ({ page, context }) => {
- test.skip(
- test.info().project.name !== 'chromium',
- 'Canonical visual baselines use Chromium; other projects run functional coverage.',
- );
- await page.setViewportSize(scenario.viewport);
- await context.addCookies([
- {
- name: 'hpc-theme',
- value: scenario.theme,
- domain: '127.0.0.1',
- path: '/',
- secure: false,
- sameSite: 'Lax',
- },
- ]);
- await page.goto(`/${scenario.locale}`);
- await expect(page).toHaveScreenshot(`${scenario.name}.png`, { fullPage: true });
- });
-}
-
-test('mobile navigation open baseline', async ({ page }) => {
- test.skip(test.info().project.name !== 'chromium');
- await page.setViewportSize({ width: 390, height: 844 });
- await page.context().addCookies([
- {
- name: 'hpc-theme',
- value: 'dark',
- domain: '127.0.0.1',
- path: '/',
- secure: false,
- sameSite: 'Lax',
- },
- ]);
- await page.goto('/ar');
- await page.getByRole('button', { name: 'فتح قائمة التنقل' }).click();
- await expect(page).toHaveScreenshot('mobile-ar-dark-navigation-open.png', { fullPage: true });
-});
-
-test('keyboard focus baseline', async ({ page }) => {
- test.skip(test.info().project.name !== 'chromium');
- await page.setViewportSize({ width: 1440, height: 900 });
- await page.goto('/en');
- await page.keyboard.press('Tab');
- await expect(page).toHaveScreenshot('desktop-en-light-focus.png', { fullPage: true });
-});
diff --git a/homepage/tsconfig.json b/homepage/tsconfig.json
deleted file mode 100644
index 7fb8402..0000000
--- a/homepage/tsconfig.json
+++ /dev/null
@@ -1,46 +0,0 @@
-{
- "compilerOptions": {
- "target": "ES2024",
- "lib": ["dom", "dom.iterable", "es2024"],
- "allowJs": false,
- "skipLibCheck": false,
- "strict": true,
- "noEmit": true,
- "esModuleInterop": true,
- "module": "esnext",
- "moduleResolution": "bundler",
- "resolveJsonModule": true,
- "isolatedModules": true,
- "jsx": "react-jsx",
- "incremental": true,
- "plugins": [
- {
- "name": "next"
- }
- ],
- "noUncheckedIndexedAccess": true,
- "exactOptionalPropertyTypes": true,
- "noImplicitOverride": true,
- "noFallthroughCasesInSwitch": true,
- "useUnknownInCatchVariables": true,
- "forceConsistentCasingInFileNames": true,
- "verbatimModuleSyntax": true,
- "paths": {
- "@/*": ["./src/*"]
- }
- },
- "include": [
- "next-env.d.ts",
- ".next/types/**/*.ts",
- "src/**/*.ts",
- "src/**/*.tsx",
- "tests/**/*.ts",
- "tests/**/*.tsx",
- "vitest.config.ts",
- "playwright.config.ts",
- "next.config.ts",
- "vitest.integration.config.ts",
- ".next/dev/types/**/*.ts"
- ],
- "exclude": ["node_modules", "contracts"]
-}
diff --git a/homepage/vitest.config.ts b/homepage/vitest.config.ts
deleted file mode 100644
index a947d44..0000000
--- a/homepage/vitest.config.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { fileURLToPath } from 'node:url';
-import { defineConfig } from 'vitest/config';
-
-export default defineConfig({
- resolve: {
- alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) },
- },
- test: {
- environment: 'jsdom',
- globals: true,
- setupFiles: ['./tests/setup.ts'],
- include: ['tests/unit/**/*.test.{ts,tsx}'],
- coverage: {
- provider: 'v8',
- reporter: ['text', 'json-summary'],
- thresholds: {
- lines: 85,
- functions: 85,
- branches: 80,
- statements: 85,
- },
- include: [
- 'src/lib/localization/config.ts',
- 'src/lib/security/csp.ts',
- 'src/lib/theme/config.ts',
- ],
- },
- },
-});
diff --git a/homepage/vitest.integration.config.ts b/homepage/vitest.integration.config.ts
deleted file mode 100644
index 777c9eb..0000000
--- a/homepage/vitest.integration.config.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { fileURLToPath } from 'node:url';
-import { defineConfig } from 'vitest/config';
-
-export default defineConfig({
- resolve: {
- alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) },
- },
- test: {
- environment: 'node',
- globals: true,
- include: ['tests/integration/**/*.test.ts'],
- },
-});