diff --git a/apps/dashboard/src/app/layout.tsx b/apps/dashboard/src/app/layout.tsx
index e1917e5..b61f6e4 100644
--- a/apps/dashboard/src/app/layout.tsx
+++ b/apps/dashboard/src/app/layout.tsx
@@ -1,10 +1,11 @@
import type { Metadata } from 'next'
import { cookies } from 'next/headers'
import { JetBrains_Mono } from 'next/font/google'
+import Script from 'next/script'
import '@fontsource-variable/inter/index.css'
import '@fontsource-variable/noto-sans-arabic/index.css'
import { DashboardI18nProvider } from '@/components/I18nProvider'
-import { SHARED_LANGUAGE_COOKIE } from '@/lib/preferences'
+import { HOMEPAGE_THEME_COOKIE, SHARED_LANGUAGE_COOKIE, SHARED_THEME_KEY } from '@/lib/preferences'
import './globals.css'
const jetbrainsMono = JetBrains_Mono({
@@ -33,14 +34,18 @@ export default async function RootLayout({ children }: { children: React.ReactNo
cookieStore.get(SHARED_LANGUAGE_COOKIE)?.value ?? cookieStore.get('dashboard-language')?.value,
)
const dir = initialLanguage === 'ar' ? 'rtl' : 'ltr'
+ const cookieTheme = cookieStore.get(SHARED_THEME_KEY)?.value ?? cookieStore.get(HOMEPAGE_THEME_COOKIE)?.value
+ const initialTheme = cookieTheme === 'light' ? 'light' : 'dark'
return (
-
+
-
diff --git a/apps/dashboard/src/components/I18nProvider.tsx b/apps/dashboard/src/components/I18nProvider.tsx
index 90a0e7a..1e72243 100644
--- a/apps/dashboard/src/components/I18nProvider.tsx
+++ b/apps/dashboard/src/components/I18nProvider.tsx
@@ -1,7 +1,7 @@
'use client'
import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react'
-import { SHARED_LANGUAGE_COOKIE, SHARED_LANGUAGE_KEY, SHARED_THEME_KEY, readCurrentUserScopedPreference, readScopedPreference, writeScopedPreference } from '@/lib/preferences'
+import { HOMEPAGE_THEME_COOKIE, HOMEPAGE_THEME_KEY, SHARED_LANGUAGE_COOKIE, SHARED_LANGUAGE_KEY, SHARED_THEME_KEY, readCurrentUserScopedPreference, readScopedPreference, writeScopedPreference } from '@/lib/preferences'
import { apiFetch } from '@/lib/api'
export type DashboardLanguage = 'en' | 'fr' | 'ar'
@@ -1427,7 +1427,7 @@ export function DashboardI18nProvider({
initialLanguage?: DashboardLanguage
}) {
const [language, setLanguageState] = useState(initialLanguage)
- const [theme, setThemeState] = useState('light')
+ const [theme, setThemeState] = useState('dark')
const themeInitialized = useRef(false)
// Skip the very first write so we don't overwrite a stored preference with the
// server-resolved initialLanguage before the read effect has a chance to apply it.
@@ -1455,7 +1455,7 @@ export function DashboardI18nProvider({
document.documentElement.classList.add(nextTheme === 'light' ? 'light' : 'dark')
document.documentElement.style.colorScheme = nextTheme === 'light' ? 'light' : 'dark'
document.body.dataset.theme = nextTheme
- writeScopedPreference(SHARED_THEME_KEY, nextTheme, ['dashboard-theme'])
+ writeScopedPreference(SHARED_THEME_KEY, nextTheme, ['dashboard-theme', HOMEPAGE_THEME_KEY], [HOMEPAGE_THEME_COOKIE])
}
setThemeState(nextTheme)
@@ -1473,15 +1473,14 @@ export function DashboardI18nProvider({
}, [])
useEffect(() => {
- const storedTheme = readScopedPreference(SHARED_THEME_KEY, ['dashboard-theme'])
+ const queryTheme = new URLSearchParams(window.location.search).get('theme')
+ const storedTheme = queryTheme === 'light' || queryTheme === 'dark'
+ ? queryTheme
+ : readScopedPreference(SHARED_THEME_KEY, ['dashboard-theme', HOMEPAGE_THEME_KEY], [HOMEPAGE_THEME_COOKIE])
if (storedTheme === 'light' || storedTheme === 'dark') {
if (storedTheme !== theme) setThemeState(storedTheme)
return
}
-
- if (window.matchMedia('(prefers-color-scheme: dark)').matches && theme !== 'dark') {
- setThemeState('dark')
- }
}, [])
useEffect(() => {
@@ -1497,16 +1496,17 @@ export function DashboardI18nProvider({
}, [language])
useEffect(() => {
- if (!themeInitialized.current) {
- // Skip first run: inline script already applied the correct class; writing 'light' here would flash and corrupt storage.
- themeInitialized.current = true
- return
- }
document.documentElement.classList.remove('light', 'dark')
document.documentElement.classList.add(theme === 'light' ? 'light' : 'dark')
document.documentElement.style.colorScheme = theme === 'light' ? 'light' : 'dark'
document.body.dataset.theme = theme
- writeScopedPreference(SHARED_THEME_KEY, theme, ['dashboard-theme'])
+
+ if (!themeInitialized.current) {
+ // Skip first write: inline script already applied the correct class; writing too early can corrupt stored preferences.
+ themeInitialized.current = true
+ return
+ }
+ writeScopedPreference(SHARED_THEME_KEY, theme, ['dashboard-theme', HOMEPAGE_THEME_KEY], [HOMEPAGE_THEME_COOKIE])
}, [theme])
useEffect(() => {
@@ -1530,7 +1530,7 @@ export function DashboardI18nProvider({
if (scopedTheme === 'light' || scopedTheme === 'dark') {
if (scopedTheme !== theme) setThemeState(scopedTheme)
} else {
- writeScopedPreference(SHARED_THEME_KEY, theme, ['dashboard-theme'])
+ writeScopedPreference(SHARED_THEME_KEY, theme, ['dashboard-theme', HOMEPAGE_THEME_KEY], [HOMEPAGE_THEME_COOKIE])
}
}
diff --git a/apps/dashboard/src/lib/preferences.test.ts b/apps/dashboard/src/lib/preferences.test.ts
index 739ed82..7292e22 100644
--- a/apps/dashboard/src/lib/preferences.test.ts
+++ b/apps/dashboard/src/lib/preferences.test.ts
@@ -84,6 +84,17 @@ describe('dashboard scoped preferences', () => {
expect(readScopedPreference('theme', ['legacy-theme'])).toBe('base')
})
+ it('can read and write legacy cookie names for cross-app preferences', () => {
+ const browser = installBrowser('hpc-theme=dark')
+
+ expect(readScopedPreference('rentaldrivego-theme', [], ['hpc-theme'])).toBe('dark')
+
+ writeScopedPreference('rentaldrivego-theme', 'light', [], ['hpc-theme'])
+
+ expect(browser.cookie).toContain('rentaldrivego-theme=light')
+ expect(browser.cookie).toContain('hpc-theme=light')
+ })
+
it('writes shared and legacy preference values without auth-token scoping', () => {
const browser = installBrowser()
diff --git a/apps/dashboard/src/lib/preferences.ts b/apps/dashboard/src/lib/preferences.ts
index 3b0b0d0..03a8d29 100644
--- a/apps/dashboard/src/lib/preferences.ts
+++ b/apps/dashboard/src/lib/preferences.ts
@@ -1,6 +1,8 @@
export const SHARED_LANGUAGE_COOKIE = 'rentaldrivego-language'
export const SHARED_LANGUAGE_KEY = 'rentaldrivego-language'
export const SHARED_THEME_KEY = 'rentaldrivego-theme'
+export const HOMEPAGE_THEME_COOKIE = 'hpc-theme'
+export const HOMEPAGE_THEME_KEY = 'hpc.theme.preference'
function readEmployeeToken() {
return null
@@ -54,7 +56,11 @@ export function readCurrentUserScopedPreference(baseKey: string) {
return window.localStorage.getItem(getScopedPreferenceKey(baseKey))
}
-export function readScopedPreference(baseKey: string, legacyKeys: string[] = []) {
+export function readScopedPreference(
+ baseKey: string,
+ legacyKeys: string[] = [],
+ legacyCookieNames: string[] = [],
+) {
if (typeof window === 'undefined') return null
const scopedCookie = readCookie(getScopedPreferenceCookieName(baseKey))
@@ -63,6 +69,11 @@ export function readScopedPreference(baseKey: string, legacyKeys: string[] = [])
const sharedCookie = readCookie(baseKey)
if (sharedCookie) return sharedCookie
+ for (const cookieName of legacyCookieNames) {
+ const legacyCookie = readCookie(cookieName)
+ if (legacyCookie) return legacyCookie
+ }
+
const scopedKey = getScopedPreferenceKey(baseKey)
const candidates = [scopedKey, baseKey, ...legacyKeys]
@@ -74,7 +85,12 @@ export function readScopedPreference(baseKey: string, legacyKeys: string[] = [])
return null
}
-export function writeScopedPreference(baseKey: string, value: string, legacyKeys: string[] = []) {
+export function writeScopedPreference(
+ baseKey: string,
+ value: string,
+ legacyKeys: string[] = [],
+ legacyCookieNames: string[] = [],
+) {
if (typeof window === 'undefined') return
const scopedKey = getScopedPreferenceKey(baseKey)
@@ -84,6 +100,9 @@ export function writeScopedPreference(baseKey: string, value: string, legacyKeys
if (scopedCookie !== baseKey) {
writeCookie(scopedCookie, value)
}
+ for (const cookieName of legacyCookieNames) {
+ writeCookie(cookieName, value)
+ }
window.localStorage.setItem(scopedKey, value)
window.localStorage.setItem(baseKey, value)
diff --git a/apps/homepage/src/app/[locale]/layout.tsx b/apps/homepage/src/app/[locale]/layout.tsx
index 23f28c5..84d7ccb 100644
--- a/apps/homepage/src/app/[locale]/layout.tsx
+++ b/apps/homepage/src/app/[locale]/layout.tsx
@@ -20,6 +20,7 @@ import {
} from '@/lib/theme/config';
import { cookies, headers } from 'next/headers';
import { notFound } from 'next/navigation';
+import Script from 'next/script';
import type { Metadata } from 'next';
import type { ReactNode } from 'react';
@@ -46,7 +47,6 @@ export default async function LocaleLayout({ children, params }: LocaleLayoutPro
const messages = getMessages(locale);
const integrationConfig = getPublicIntegrationConfig();
const requestHeaders = await headers();
- const nonce = requestHeaders.get('x-nonce') ?? undefined;
const cookieStore = await cookies();
const headerPreference = requestHeaders.get('x-theme-preference');
const cookiePreference = cookieStore.get(themeCookie)?.value;
@@ -54,7 +54,7 @@ export default async function LocaleLayout({ children, params }: LocaleLayoutPro
? headerPreference
: isThemePreference(cookiePreference)
? cookiePreference
- : 'system';
+ : 'dark';
const resolvedTheme = serverResolvedTheme(preference);
return (
@@ -67,9 +67,9 @@ export default async function LocaleLayout({ children, params }: LocaleLayoutPro
suppressHydrationWarning
>
-
diff --git a/apps/homepage/src/components/app-shell/AuthHeader.tsx b/apps/homepage/src/components/app-shell/AuthHeader.tsx
index d98617b..2e9457d 100644
--- a/apps/homepage/src/components/app-shell/AuthHeader.tsx
+++ b/apps/homepage/src/components/app-shell/AuthHeader.tsx
@@ -6,6 +6,7 @@ import Image from 'next/image';
import { LocalePill } from './LocalePill';
import { ThemePill } from './ThemePill';
import styles from './AuthHeader.module.css';
+import { useThemePreference } from './useThemePreference';
interface Props {
locale: Locale;
@@ -13,7 +14,8 @@ interface Props {
}
export function AuthHeader({ locale, themePreference }: Props) {
- const homePath = localizedModePath('home', locale, themePreference);
+ const currentPreference = useThemePreference(themePreference);
+ const homePath = localizedModePath('home', locale, currentPreference);
return (
@@ -35,7 +37,7 @@ export function AuthHeader({ locale, themePreference }: Props) {
-
+
diff --git a/apps/homepage/src/components/app-shell/BrandLink.tsx b/apps/homepage/src/components/app-shell/BrandLink.tsx
index 7d9403d..62c9cfb 100644
--- a/apps/homepage/src/components/app-shell/BrandLink.tsx
+++ b/apps/homepage/src/components/app-shell/BrandLink.tsx
@@ -5,6 +5,7 @@ import type { ThemePreference } from '@/lib/theme/config';
import Image from 'next/image';
import { usePathname } from 'next/navigation';
import styles from './SiteHeader.module.css';
+import { useThemePreference } from './useThemePreference';
export function BrandLink({
locale,
@@ -16,10 +17,11 @@ export function BrandLink({
label: string;
}) {
const pathname = usePathname();
+ const currentMode = useThemePreference(mode);
return (
diff --git a/apps/homepage/src/components/app-shell/HeaderNavigation.tsx b/apps/homepage/src/components/app-shell/HeaderNavigation.tsx
index 5e6d0ce..dccc5bd 100644
--- a/apps/homepage/src/components/app-shell/HeaderNavigation.tsx
+++ b/apps/homepage/src/components/app-shell/HeaderNavigation.tsx
@@ -5,6 +5,7 @@ import type { ThemePreference } from '@/lib/theme/config';
import { usePathname } from 'next/navigation';
import { useEffect, useState } from 'react';
import styles from './SiteHeader.module.css';
+import { useThemePreference } from './useThemePreference';
export type HeaderNavId = 'product' | 'workflow' | 'modules' | 'pricing' | 'faq';
@@ -30,6 +31,7 @@ export function HeaderNavigation({
const pathname = usePathname();
const currentRoute = routeIdFromPathname(pathname);
const [currentHash, setCurrentHash] = useState('');
+ const currentMode = useThemePreference(mode);
useEffect(() => {
const updateHash = () => setCurrentHash(window.location.hash.replace(/^#/, ''));
@@ -38,7 +40,7 @@ export function HeaderNavigation({
return () => window.removeEventListener('hashchange', updateHash);
}, []);
- const home = localizedModePath('home', locale, mode);
+ const home = localizedModePath('home', locale, currentMode);
return (
-
+
{messages.header.login}
-
-
+
+
{messages.header.demo}
-
+
diff --git a/apps/homepage/src/components/app-shell/SiteFooter.tsx b/apps/homepage/src/components/app-shell/SiteFooter.tsx
index b06aa1f..fd484d7 100644
--- a/apps/homepage/src/components/app-shell/SiteFooter.tsx
+++ b/apps/homepage/src/components/app-shell/SiteFooter.tsx
@@ -117,7 +117,7 @@ export function SiteFooter({ locale, mode, messages }: SiteFooterProps) {
- {footer.links.demo}
+ {footer.links.demo}
diff --git a/apps/homepage/src/components/app-shell/SiteHeader.tsx b/apps/homepage/src/components/app-shell/SiteHeader.tsx
index 2245b2a..57d2a2f 100644
--- a/apps/homepage/src/components/app-shell/SiteHeader.tsx
+++ b/apps/homepage/src/components/app-shell/SiteHeader.tsx
@@ -1,18 +1,14 @@
-import { localizedModePath, type Locale } from '@/lib/localization/config';
+import { 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 { accountCreateUrl } from '@/lib/account-urls';
import { BrandLink } from './BrandLink';
import { HeaderNavigation } from './HeaderNavigation';
import { LocalePill } from './LocalePill';
import { MobileNavigation } from './MobileNavigation';
import styles from './SiteHeader.module.css';
+import { ThemedAccountCreateLink } from './ThemedAccountCreateLink';
import { ThemePill } from './ThemePill';
-
-function signInUrl(locale: Locale, mode: ThemePreference): string {
- return localizedModePath('sign-in', locale, mode);
-}
+import { ThemedRouteActionLink } from './ThemedRouteActionLink';
interface SiteHeaderProps {
locale: Locale;
@@ -40,24 +36,34 @@ export function SiteHeader({ locale, messages, themePreference }: SiteHeaderProp
role="group"
aria-label={messages.header.controlsLabel}
>
-
+
-
+
{messages.header.login}
-
-
+
+
{messages.header.demo}
-
+
-
{messages.header.demo}
-
+
, 'href'> {
+ locale: Locale;
+ initialPreference: ThemePreference;
+}
+
+export function ThemedAccountCreateLink({
+ locale,
+ initialPreference,
+ ...props
+}: ThemedAccountCreateLinkProps) {
+ const preference = useThemePreference(initialPreference);
+ return ;
+}
diff --git a/apps/homepage/src/components/app-shell/ThemedRouteActionLink.tsx b/apps/homepage/src/components/app-shell/ThemedRouteActionLink.tsx
new file mode 100644
index 0000000..e440bae
--- /dev/null
+++ b/apps/homepage/src/components/app-shell/ThemedRouteActionLink.tsx
@@ -0,0 +1,24 @@
+'use client';
+
+import { ActionLink } from '@/components/actions/ActionLink';
+import { localizedModePath, type Locale, type RouteId } from '@/lib/localization/config';
+import type { ThemePreference } from '@/lib/theme/config';
+import type { ComponentProps } from 'react';
+import { useThemePreference } from './useThemePreference';
+
+interface ThemedRouteActionLinkProps
+ extends Omit, 'href'> {
+ locale: Locale;
+ routeId: RouteId;
+ initialPreference: ThemePreference;
+}
+
+export function ThemedRouteActionLink({
+ locale,
+ routeId,
+ initialPreference,
+ ...props
+}: ThemedRouteActionLinkProps) {
+ const preference = useThemePreference(initialPreference);
+ return ;
+}
diff --git a/apps/homepage/src/components/app-shell/useThemePreference.ts b/apps/homepage/src/components/app-shell/useThemePreference.ts
new file mode 100644
index 0000000..19990a2
--- /dev/null
+++ b/apps/homepage/src/components/app-shell/useThemePreference.ts
@@ -0,0 +1,25 @@
+'use client';
+
+import { currentThemePreference, themePreferenceEvent } from '@/lib/theme/client';
+import type { ThemePreference } from '@/lib/theme/config';
+import { useEffect, useState } from 'react';
+
+export function useThemePreference(initialPreference: ThemePreference): ThemePreference {
+ const [preference, setPreference] = useState(initialPreference);
+
+ useEffect(() => {
+ setPreference(currentThemePreference());
+
+ const handlePreference = (event: Event) => {
+ const next = (event as CustomEvent).detail;
+ if (next === 'light' || next === 'dark' || next === 'system') {
+ setPreference(next);
+ }
+ };
+
+ window.addEventListener(themePreferenceEvent, handlePreference);
+ return () => window.removeEventListener(themePreferenceEvent, handlePreference);
+ }, []);
+
+ return preference;
+}
diff --git a/apps/homepage/src/components/auth/AuthForms.module.css b/apps/homepage/src/components/auth/AuthForms.module.css
index a28aa50..6581586 100644
--- a/apps/homepage/src/components/auth/AuthForms.module.css
+++ b/apps/homepage/src/components/auth/AuthForms.module.css
@@ -140,3 +140,22 @@
margin-block-start: 1rem;
text-align: center;
}
+
+.dashboardOrangeButton.dashboardOrangeButton {
+ border-color: transparent;
+ background: #ea580c;
+ color: #ffffff;
+}
+
+.dashboardOrangeButton.dashboardOrangeButton:hover:not(:disabled) {
+ background: #c2410c;
+}
+
+:global(html[data-theme='dark']) .dashboardOrangeButton.dashboardOrangeButton {
+ background: #f97316;
+ color: #ffffff;
+}
+
+:global(html[data-theme='dark']) .dashboardOrangeButton.dashboardOrangeButton:hover:not(:disabled) {
+ background: #fb923c;
+}
diff --git a/apps/homepage/src/components/auth/ForgotPasswordForm.tsx b/apps/homepage/src/components/auth/ForgotPasswordForm.tsx
index 36ea275..409e735 100644
--- a/apps/homepage/src/components/auth/ForgotPasswordForm.tsx
+++ b/apps/homepage/src/components/auth/ForgotPasswordForm.tsx
@@ -140,7 +140,14 @@ export function ForgotPasswordForm({ locale }: { locale: Locale }) {
/>
-
-
+
{loading ? dict.signingIn : dict.signIn}
@@ -336,7 +343,14 @@ export function SignInForm({ locale }: { locale: Locale }) {
/>
-
+
{loading ? dict.verifying : dict.verify}
diff --git a/apps/homepage/src/content/homepage-model.ts b/apps/homepage/src/content/homepage-model.ts
index 2520246..8b70415 100644
--- a/apps/homepage/src/content/homepage-model.ts
+++ b/apps/homepage/src/content/homepage-model.ts
@@ -274,7 +274,7 @@ export function buildHomepageContent(
eyebrow: homepage.hero.eyebrow,
title: homepage.hero.title,
body: homepage.hero.body,
- primary: { label: homepage.hero.primary, href: accountCreateUrl(locale) },
+ primary: { label: homepage.hero.primary, href: accountCreateUrl(locale, 'dark') },
secondary: { label: homepage.hero.secondary, disabledReason: pendingReason },
preview: {
title: homepage.preview.title,
@@ -436,7 +436,7 @@ export function buildHomepageContent(
eyebrow: homepage.final.eyebrow,
title: homepage.final.title,
body: homepage.final.body,
- primary: { label: homepage.final.primary, href: accountCreateUrl(locale) },
+ primary: { label: homepage.final.primary, href: accountCreateUrl(locale, 'dark') },
secondary: { label: homepage.final.secondary, disabledReason: pendingReason },
image: localizedImage(
'rentaldrivego',
diff --git a/apps/homepage/src/lib/account-urls.ts b/apps/homepage/src/lib/account-urls.ts
index 48c681a..17cb61c 100644
--- a/apps/homepage/src/lib/account-urls.ts
+++ b/apps/homepage/src/lib/account-urls.ts
@@ -1,6 +1,8 @@
import type { Locale } from '@/lib/localization/config';
+import type { ThemePreference } from '@/lib/theme/config';
-export function accountCreateUrl(locale: Locale): string {
+export function accountCreateUrl(locale: Locale, theme?: ThemePreference): string {
const params = new URLSearchParams({ lang: locale });
+ if (theme === 'light' || theme === 'dark') params.set('theme', theme);
return `/dashboard/sign-up?${params.toString()}`;
}
diff --git a/apps/homepage/src/lib/localization/config.ts b/apps/homepage/src/lib/localization/config.ts
index 30d703a..0c6eda8 100644
--- a/apps/homepage/src/lib/localization/config.ts
+++ b/apps/homepage/src/lib/localization/config.ts
@@ -12,7 +12,7 @@ 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 defaultMode: ThemePreference = 'system';
+export const defaultMode: ThemePreference = 'dark';
export const localeCookie = 'hpc-locale';
export const localeStorageKey = 'hpc.locale';
export const localeCookieMaxAgeSeconds = 31_536_000;
diff --git a/apps/homepage/src/lib/theme/bootstrap-script.ts b/apps/homepage/src/lib/theme/bootstrap-script.ts
index 137cf3b..bbc17e8 100644
--- a/apps/homepage/src/lib/theme/bootstrap-script.ts
+++ b/apps/homepage/src/lib/theme/bootstrap-script.ts
@@ -4,7 +4,7 @@ export const themeBootstrapScript = `(function () {
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 preference = allowed[cookieValue] ? cookieValue : (allowed[stored] ? stored : "dark");
var dark = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
var resolved = preference === "system" ? (dark ? "dark" : "light") : preference;
var root = document.documentElement;
diff --git a/apps/homepage/src/lib/theme/client.ts b/apps/homepage/src/lib/theme/client.ts
index d9eae63..a980e9f 100644
--- a/apps/homepage/src/lib/theme/client.ts
+++ b/apps/homepage/src/lib/theme/client.ts
@@ -40,5 +40,5 @@ export function persistTheme(preference: ThemePreference): void {
export function currentThemePreference(): ThemePreference {
const value = document.documentElement.dataset.themePreference;
- return isThemePreference(value) ? value : 'system';
+ return isThemePreference(value) ? value : 'dark';
}
diff --git a/apps/homepage/src/proxy.ts b/apps/homepage/src/proxy.ts
index a6f2072..e90f9ce 100644
--- a/apps/homepage/src/proxy.ts
+++ b/apps/homepage/src/proxy.ts
@@ -100,7 +100,7 @@ export function proxy(request: NextRequest): NextResponse {
const pathSegments = request.nextUrl.pathname.split('/').filter(Boolean);
- // Redirect locale-less paths (e.g. /sign-in → /en/system/sign-in), but not proxied app paths or files.
+ // Redirect locale-less paths (e.g. /sign-in -> /en/dark/sign-in), but not proxied app paths or files.
const firstSegment = request.nextUrl.pathname.split('/')[1];
const isFile = /\.\w+$/.test(request.nextUrl.pathname);
if (firstSegment && !isFile && !isLocale(firstSegment) && !reservedProxySegments.has(firstSegment)) {
diff --git a/apps/homepage/tests/browser/shell.spec.ts b/apps/homepage/tests/browser/shell.spec.ts
index 8d501e3..a1a0980 100644
--- a/apps/homepage/tests/browser/shell.spec.ts
+++ b/apps/homepage/tests/browser/shell.spec.ts
@@ -41,7 +41,7 @@ test('root locale detection honors Accept-Language', async ({ browser }) => {
});
const page = await context.newPage();
await page.goto('/');
- await expect(page).toHaveURL(/\/ar\/system$/);
+ await expect(page).toHaveURL(/\/ar\/dark$/);
await context.close();
});
diff --git a/apps/homepage/tests/browser/theme-csp.spec.ts b/apps/homepage/tests/browser/theme-csp.spec.ts
index cf9028f..88f770e 100644
--- a/apps/homepage/tests/browser/theme-csp.spec.ts
+++ b/apps/homepage/tests/browser/theme-csp.spec.ts
@@ -18,8 +18,7 @@ test('explicit dark theme is present on the server-rendered root', async ({ brow
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();
+ await expect(page.locator('#theme-bootstrap')).not.toHaveAttribute('nonce');
expect(csp).toContain("'unsafe-inline'");
expect(csp).not.toContain("'strict-dynamic'");
await context.close();
diff --git a/apps/homepage/tests/unit/components/header-navigation.test.tsx b/apps/homepage/tests/unit/components/header-navigation.test.tsx
new file mode 100644
index 0000000..9e46dd9
--- /dev/null
+++ b/apps/homepage/tests/unit/components/header-navigation.test.tsx
@@ -0,0 +1,65 @@
+import { HeaderNavigation } from '@/components/app-shell/HeaderNavigation';
+import { ThemedAccountCreateLink } from '@/components/app-shell/ThemedAccountCreateLink';
+import { act, render, screen } from '@testing-library/react';
+import { describe, expect, it, vi } from 'vitest';
+
+vi.mock('next/navigation', () => ({
+ usePathname: () => '/en/light',
+}));
+
+const labels = {
+ product: 'Product',
+ workflow: 'Workflow',
+ modules: 'Modules',
+ pricing: 'Pricing',
+ faq: 'FAQ',
+};
+
+describe('HeaderNavigation', () => {
+ it('updates destinations when the client theme preference changes', async () => {
+ document.documentElement.dataset.themePreference = 'light';
+
+ render(
+ ,
+ );
+
+ const product = screen.getByRole('link', { name: 'Product' });
+ expect(product).toHaveAttribute('href', '/en/light#product');
+
+ await act(async () => {
+ document.documentElement.dataset.themePreference = 'dark';
+ window.dispatchEvent(
+ new CustomEvent('rentaldrivego:theme-preference', { detail: 'dark' }),
+ );
+ });
+
+ expect(product).toHaveAttribute('href', '/en/dark#product');
+ });
+
+ it('updates account creation destinations when the client theme preference changes', async () => {
+ document.documentElement.dataset.themePreference = 'light';
+
+ render(
+
+ Create account
+ ,
+ );
+
+ const link = screen.getByRole('link', { name: 'Create account' });
+ expect(link).toHaveAttribute('href', '/dashboard/sign-up?lang=en&theme=light');
+
+ await act(async () => {
+ document.documentElement.dataset.themePreference = 'dark';
+ window.dispatchEvent(
+ new CustomEvent('rentaldrivego:theme-preference', { detail: 'dark' }),
+ );
+ });
+
+ expect(link).toHaveAttribute('href', '/dashboard/sign-up?lang=en&theme=dark');
+ });
+});
diff --git a/apps/homepage/tests/unit/homepage-content.test.ts b/apps/homepage/tests/unit/homepage-content.test.ts
index 940ca45..81d538f 100644
--- a/apps/homepage/tests/unit/homepage-content.test.ts
+++ b/apps/homepage/tests/unit/homepage-content.test.ts
@@ -5,6 +5,7 @@ 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 { accountCreateUrl } from '@/lib/account-urls';
import { describe, expect, it } from 'vitest';
describe('homepage content model', () => {
@@ -28,14 +29,19 @@ describe('homepage content model', () => {
it('keeps unresolved conversion destinations disabled', () => {
const content = buildHomepageContent(enHomepage, enShell);
- expect(content.hero.primary.href).toBe('/dashboard/sign-up?lang=en');
+ expect(content.hero.primary.href).toBe('/dashboard/sign-up?lang=en&theme=dark');
expect(content.hero.primary.disabledReason).toBeUndefined();
- expect(content.final.primary.href).toBe('/dashboard/sign-up?lang=en');
+ expect(content.final.primary.href).toBe('/dashboard/sign-up?lang=en&theme=dark');
expect(content.final.primary.disabledReason).toBeUndefined();
expect(content.pricing.actionDisabledReason).toBe(enShell.states.pendingBody);
expect(content.final.secondary.disabledReason).toBe(enShell.states.pendingBody);
});
+ it('includes explicit theme handoff in account creation URLs', () => {
+ expect(accountCreateUrl('en', 'light')).toBe('/dashboard/sign-up?lang=en&theme=light');
+ expect(accountCreateUrl('en', 'dark')).toBe('/dashboard/sign-up?lang=en&theme=dark');
+ });
+
it('keeps preview identifiers isolated from translated prose', () => {
const content = buildHomepageContent(arHomepage, arShell);
diff --git a/apps/homepage/tests/unit/integrations/destinations.test.ts b/apps/homepage/tests/unit/integrations/destinations.test.ts
index 23d8639..bde8e94 100644
--- a/apps/homepage/tests/unit/integrations/destinations.test.ts
+++ b/apps/homepage/tests/unit/integrations/destinations.test.ts
@@ -19,9 +19,9 @@ describe('destination registry', () => {
expect(
resolveDestination('privacy', 'fr', { demoSubmissionEnabled: false, demoMode: 'blocked' })
.href,
- ).toBe('/fr/system/confidentialite');
+ ).toBe('/fr/dark/confidentialite');
expect(
resolveDestination('sign-in', 'en', { demoSubmissionEnabled: false, demoMode: 'blocked' }).href,
- ).toBe('/en/system/sign-in');
+ ).toBe('/en/dark/sign-in');
});
});
diff --git a/apps/homepage/tests/unit/localization.test.ts b/apps/homepage/tests/unit/localization.test.ts
index 2176d1c..b836691 100644
--- a/apps/homepage/tests/unit/localization.test.ts
+++ b/apps/homepage/tests/unit/localization.test.ts
@@ -27,9 +27,9 @@ describe('localization foundations', () => {
});
it('generates and resolves localized stable route paths', () => {
- expect(localizedPath('home', 'ar')).toBe('/ar/system');
- expect(localizedPath('privacy', 'fr')).toBe('/fr/system/confidentialite');
- expect(localizedPath('sign-in', 'ar')).toBe('/ar/system/تسجيل-الدخول');
+ expect(localizedPath('home', 'ar')).toBe('/ar/dark');
+ expect(localizedPath('privacy', 'fr')).toBe('/fr/dark/confidentialite');
+ expect(localizedPath('sign-in', 'ar')).toBe('/ar/dark/تسجيل-الدخول');
expect(routeIdFromSlug('fr', 'confidentialite')).toBe('privacy');
expect(routeIdFromSlug('fr', 'privacy')).toBeNull();
expect(routeIdFromAnyLocaleSlug('privacy')).toBe('privacy');
@@ -43,17 +43,17 @@ describe('localization foundations', () => {
it('generates reciprocal hreflang values', () => {
const links = hreflangMap(new URL('https://approved.test'), 'home');
- expect(links.en).toBe('https://approved.test/en/system');
- expect(links.ar).toBe('https://approved.test/ar/system');
- expect(links['x-default']).toBe('https://approved.test/en/system');
+ expect(links.en).toBe('https://approved.test/en/dark');
+ expect(links.ar).toBe('https://approved.test/ar/dark');
+ expect(links['x-default']).toBe('https://approved.test/en/dark');
});
it('preserves only approved campaign query values and valid hashes', () => {
const current = new URL(
- 'https://approved.test/en/system?utm_source=campaign&email=private%40approved.test#gibberish',
+ 'https://approved.test/en/dark?utm_source=campaign&email=private%40approved.test#gibberish',
);
const switched = localeSwitchUrl(current, 'fr', 'home');
- expect(switched.pathname).toBe('/fr/system');
+ expect(switched.pathname).toBe('/fr/dark');
expect(switched.search).toBe('?utm_source=campaign');
expect(switched.hash).toBe('');
diff --git a/apps/homepage/tests/unit/proxy.test.ts b/apps/homepage/tests/unit/proxy.test.ts
index c958710..c5b772d 100644
--- a/apps/homepage/tests/unit/proxy.test.ts
+++ b/apps/homepage/tests/unit/proxy.test.ts
@@ -76,7 +76,7 @@ describe('homepage proxy app boundaries', () => {
expect(response).toMatchObject({
kind: 'redirect',
- url: 'https://rentaldrivego.ma/fr/system',
+ url: 'https://rentaldrivego.ma/fr/dark',
});
});
@@ -87,7 +87,7 @@ describe('homepage proxy app boundaries', () => {
expect(response).toMatchObject({
kind: 'redirect',
- url: 'https://rentaldrivego.ma/en/system/privacy',
+ url: 'https://rentaldrivego.ma/en/dark/privacy',
});
});
diff --git a/apps/homepage/tests/unit/theme.test.ts b/apps/homepage/tests/unit/theme.test.ts
index c4f374f..270218f 100644
--- a/apps/homepage/tests/unit/theme.test.ts
+++ b/apps/homepage/tests/unit/theme.test.ts
@@ -1,3 +1,4 @@
+import { currentThemePreference } from '@/lib/theme/client';
import { isThemePreference, resolveTheme, serverResolvedTheme } from '@/lib/theme/config';
import { describe, expect, it } from 'vitest';
@@ -15,4 +16,9 @@ describe('theme foundations', () => {
expect(resolveTheme('light', true)).toBe('light');
expect(serverResolvedTheme('system')).toBe('light');
});
+
+ it('defaults client theme preference to dark when no choice exists', () => {
+ delete document.documentElement.dataset.themePreference;
+ expect(currentThemePreference()).toBe('dark');
+ });
});