update dark and ligth theme

This commit is contained in:
2026-08-10 12:35:18 +05:30
parent f397e94418
commit 6cc3c9a0b7
28 changed files with 319 additions and 105 deletions

View File

@@ -19,7 +19,10 @@ import {ChartTooltip} from './ChartTooltip';
*/
export const AXIS_TICK = {
fill: '#e4e4e7',
// recharts writes this straight onto the SVG fill attribute, where var()
// resolves natively — so ticks follow the mode instead of staying at the
// one light gray that only ever read on a dark canvas.
fill: CHART.axis,
fontSize: 12,
fontWeight: 500,
} as const;

View File

@@ -152,41 +152,41 @@ export function DownloadDropdown({
left: `${coords.left}px`,
zIndex: 99999,
}}
className="w-56 rounded-xl border border-zinc-700 bg-[#18181b] backdrop-blur-xl shadow-2xl z-[99999] py-1.5 divide-y divide-zinc-800 select-none"
className="w-56 rounded-xl border border-border-strong bg-popover backdrop-blur-xl shadow-2xl z-[99999] py-1.5 divide-y divide-border select-none"
>
<button
type="button"
className="w-full text-left px-4 py-3 hover:bg-white/10 flex items-center gap-3 transition-colors cursor-pointer"
className="w-full text-left px-4 py-3 hover:bg-overlay-pressed flex items-center gap-3 transition-colors cursor-pointer"
onClick={() => handleSelectFormat('pdf')}
>
<span className="text-xl">📄</span>
<div>
<div className="font-semibold text-sm text-white">PDF Document</div>
<div className="text-xs text-zinc-300">.pdf file format</div>
<div className="font-semibold text-sm text-primary">PDF Document</div>
<div className="text-xs text-secondary">.pdf file format</div>
</div>
</button>
<button
type="button"
className="w-full text-left px-4 py-3 hover:bg-white/10 flex items-center gap-3 transition-colors cursor-pointer"
className="w-full text-left px-4 py-3 hover:bg-overlay-pressed flex items-center gap-3 transition-colors cursor-pointer"
onClick={() => handleSelectFormat('excel')}
>
<span className="text-xl">📊</span>
<div>
<div className="font-semibold text-sm text-white">Excel Spreadsheet</div>
<div className="text-xs text-zinc-300">.xlsx file format</div>
<div className="font-semibold text-sm text-primary">Excel Spreadsheet</div>
<div className="text-xs text-secondary">.xlsx file format</div>
</div>
</button>
<button
type="button"
className="w-full text-left px-4 py-3 hover:bg-white/10 flex items-center gap-3 transition-colors cursor-pointer"
className="w-full text-left px-4 py-3 hover:bg-overlay-pressed flex items-center gap-3 transition-colors cursor-pointer"
onClick={() => handleSelectFormat('csv')}
>
<span className="text-xl">📑</span>
<div>
<div className="font-semibold text-sm text-white">CSV Data File</div>
<div className="text-xs text-zinc-300">.csv file format</div>
<div className="font-semibold text-sm text-primary">CSV Data File</div>
<div className="text-xs text-secondary">.csv file format</div>
</div>
</button>
</div>,

View File

@@ -65,7 +65,7 @@ import {useAccountActions} from './useAccountActions';
const LAYER_OFFSET = 'mb-2';
/** The chip. Rounded like a nav item, and lit while its menu is open. */
const CHIP = 'rounded-lg transition-colors duration-150 cursor-pointer hover:bg-white/[0.06] p-2';
const CHIP = 'rounded-lg transition-colors duration-150 cursor-pointer hover:bg-overlay-hover p-2';
/**
* The collapsed chip, sized to the rail rather than to IconButton's default.

View File

@@ -0,0 +1,62 @@
'use client';
import {createContext, useCallback, useContext, useMemo, useState} from 'react';
import {
THEME_MODE_COOKIE,
THEME_MODE_MAX_AGE,
type ThemeMode,
} from '@/shared/theme/themeMode';
/**
* Holds the workspace colour mode and persists it to a cookie.
*
* This sits ABOVE <Theme> in app/providers.tsx — it has to, because <Theme>
* takes the mode as a prop and cannot read a context rendered inside itself.
* Everything downstream of the mode change is CSS: <Theme> flips
* `color-scheme` on its wrapper and on <html>, and every colour token in
* loyaly.css is a `light-dark()` pair, so the whole surface repaints without
* a single component re-reading a colour.
*
* The cookie (not localStorage) is what lets the server render the chosen
* mode on the first byte — see @/shared/theme/themeMode.
*/
interface ThemeModeValue {
/** What the user picked, including the literal 'system'. */
mode: ThemeMode;
setMode: (mode: ThemeMode) => void;
}
const ThemeModeContext = createContext<ThemeModeValue | null>(null);
ThemeModeContext.displayName = 'ThemeModeContext';
export function ThemeModeProvider({
initialMode,
children,
}: {
/** Read from the cookie in the root layout, so SSR paints it correctly. */
initialMode: ThemeMode;
children: React.ReactNode;
}) {
const [mode, setModeState] = useState<ThemeMode>(initialMode);
const setMode = useCallback((next: ThemeMode) => {
setModeState(next);
// Written directly rather than through a server action: this is a display
// preference with no server-side consequence, and a round trip would put
// a network hop between the click and the repaint.
document.cookie = `${THEME_MODE_COOKIE}=${next}; path=/; max-age=${THEME_MODE_MAX_AGE}; samesite=lax`;
}, []);
const value = useMemo(() => ({mode, setMode}), [mode, setMode]);
return <ThemeModeContext value={value}>{children}</ThemeModeContext>;
}
export function useThemeMode(): ThemeModeValue {
const ctx = useContext(ThemeModeContext);
if (!ctx) {
throw new Error('useThemeMode must be used within <ThemeModeProvider>');
}
return ctx;
}

View File

@@ -0,0 +1,45 @@
/**
* The colour-mode contract, shared by the server layout and the client
* provider. Kept free of 'use client' and of any React import so the root
* layout (a server component) can read the cookie without pulling the
* provider module into the server graph.
*
* WHY A COOKIE AND NOT localStorage
* <Theme> renders a wrapper element whose color-scheme comes from the mode
* prop, so the mode has to be known during SSR — a localStorage read only
* happens after hydration, which would mean either a flash of the wrong
* scheme or a hydration mismatch on that wrapper. The cookie travels with
* the request, so the server renders the mode the user actually chose.
*/
export type ThemeMode = 'dark' | 'light' | 'system';
export const THEME_MODE_COOKIE = 'loyaly.theme-mode';
/** One year — a display preference should outlive the session cookie. */
export const THEME_MODE_MAX_AGE = 60 * 60 * 24 * 365;
/**
* Dark is the product's identity, so it is what an unknown visitor gets —
* not 'system'. Only an explicit choice moves off it.
*/
export const DEFAULT_THEME_MODE: ThemeMode = 'dark';
export function parseThemeMode(value: string | undefined | null): ThemeMode {
return value === 'light' || value === 'dark' || value === 'system'
? value
: DEFAULT_THEME_MODE;
}
/**
* The value for `<html data-theme>`.
*
* 'system' deliberately maps to undefined: the attribute's whole job is to
* pin `color-scheme`, and system mode wants `light dark` — i.e. no pin — so
* that `light-dark()` follows the OS. This mirrors what Astryx's <Theme>
* does to documentElement after hydration; matching it here is what keeps
* the server and client attribute identical.
*/
export function htmlThemeAttr(mode: ThemeMode): 'light' | 'dark' | undefined {
return mode === 'system' ? undefined : mode;
}