update dark and ligth theme
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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.
|
||||
|
||||
62
src/shared/providers/ThemeModeProvider.tsx
Normal file
62
src/shared/providers/ThemeModeProvider.tsx
Normal 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;
|
||||
}
|
||||
45
src/shared/theme/themeMode.ts
Normal file
45
src/shared/theme/themeMode.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user