mirror of
https://github.com/SigNoz/signoz.git
synced 2026-05-25 11:20:34 +01:00
Compare commits
2 Commits
e2e/table-
...
feat/add-r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f082821ac2 | ||
|
|
3c5bd81421 |
@@ -10,8 +10,14 @@ import {
|
||||
} from '@signozhq/ui/command';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { useThemeMode } from 'hooks/useDarkMode';
|
||||
import { THEME_MODE } from 'hooks/useDarkMode/constant';
|
||||
import history from 'lib/history';
|
||||
import { ROLES as UserRole } from 'types/roles';
|
||||
import {
|
||||
canAnimateThemeRipple,
|
||||
getRippleOrigin,
|
||||
runThemeRipple,
|
||||
} from 'utils/themeRipple';
|
||||
|
||||
import { createShortcutActions } from '../../constants/shortcutActions';
|
||||
import { useCmdK } from '../../providers/cmdKProvider';
|
||||
@@ -66,12 +72,31 @@ export function CmdKPalette({
|
||||
|
||||
function handleThemeChange(value: string): void {
|
||||
logEvent('Account Settings: Theme Changed', { theme: value });
|
||||
if (value === 'auto') {
|
||||
setAutoSwitch(true);
|
||||
} else {
|
||||
setAutoSwitch(false);
|
||||
setTheme(value);
|
||||
|
||||
const currentIsDark = theme === THEME_MODE.DARK;
|
||||
const targetIsDark = value === THEME_MODE.DARK;
|
||||
const willFlipDarkMode =
|
||||
value !== THEME_MODE.SYSTEM && targetIsDark !== currentIsDark;
|
||||
|
||||
const applyChange = (): void => {
|
||||
if (value === THEME_MODE.SYSTEM) {
|
||||
setAutoSwitch(true);
|
||||
} else {
|
||||
setAutoSwitch(false);
|
||||
setTheme(value);
|
||||
}
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
if (!willFlipDarkMode || !canAnimateThemeRipple()) {
|
||||
applyChange();
|
||||
return;
|
||||
}
|
||||
|
||||
const activeItem = document.querySelector<HTMLElement>(
|
||||
'[cmdk-item][data-selected="true"]',
|
||||
);
|
||||
runThemeRipple(getRippleOrigin(activeItem), applyChange);
|
||||
}
|
||||
|
||||
function onClickHandler(key: string): void {
|
||||
|
||||
@@ -14,6 +14,12 @@ import { useAppContext } from 'providers/App/App';
|
||||
import { UserPreference } from 'types/api/preferences/preference';
|
||||
import { showErrorNotification } from 'utils/error';
|
||||
|
||||
import {
|
||||
canAnimateThemeRipple,
|
||||
getRippleOrigin,
|
||||
runThemeRipple,
|
||||
} from 'utils/themeRipple';
|
||||
|
||||
import LicenseSection from './LicenseSection';
|
||||
import TimezoneAdaptation from './TimezoneAdaptation/TimezoneAdaptation';
|
||||
import UserInfo from './UserInfo';
|
||||
@@ -88,22 +94,35 @@ function MySettings(): JSX.Element {
|
||||
return isDarkMode ? 'dark' : 'light';
|
||||
});
|
||||
|
||||
const handleThemeChange = ({ target: { value } }: RadioChangeEvent): void => {
|
||||
logEvent('Account Settings: Theme Changed', {
|
||||
theme: value,
|
||||
});
|
||||
setTheme(value);
|
||||
const handleThemeChange = (event: RadioChangeEvent): void => {
|
||||
const { value } = event.target;
|
||||
logEvent('Account Settings: Theme Changed', { theme: value });
|
||||
|
||||
if (value === 'auto') {
|
||||
setAutoSwitch(true);
|
||||
} else {
|
||||
const willFlipDarkMode =
|
||||
value !== 'auto' && (value === 'dark') !== isDarkMode;
|
||||
|
||||
const applyChange = (): void => {
|
||||
setTheme(value);
|
||||
if (value === 'auto') {
|
||||
setAutoSwitch(true);
|
||||
return;
|
||||
}
|
||||
setAutoSwitch(false);
|
||||
// Only toggle if the current theme is different from the target
|
||||
const targetIsDark = value === 'dark';
|
||||
if (targetIsDark !== isDarkMode) {
|
||||
if (willFlipDarkMode) {
|
||||
toggleTheme();
|
||||
}
|
||||
};
|
||||
|
||||
// Only ripple on a real dark↔light flip, and only when the browser
|
||||
// supports View Transitions and the user hasn't opted out of motion.
|
||||
if (!willFlipDarkMode || !canAnimateThemeRipple()) {
|
||||
applyChange();
|
||||
return;
|
||||
}
|
||||
|
||||
const clickedTarget = event.nativeEvent?.target as HTMLElement | null;
|
||||
const clickedButton = clickedTarget?.closest('.ant-radio-button-wrapper');
|
||||
runThemeRipple(getRippleOrigin(clickedButton), applyChange);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -143,7 +162,7 @@ function MySettings(): JSX.Element {
|
||||
value: checked,
|
||||
},
|
||||
{
|
||||
onError: (error) => {
|
||||
onError: (error: unknown) => {
|
||||
// Revert the state if the API call fails
|
||||
setSideNavPinned(!checked);
|
||||
updateUserPreferenceInContext({
|
||||
|
||||
@@ -826,3 +826,18 @@ body.ai-assistant-panel-open {
|
||||
:root {
|
||||
--input-focus-outline-width: 0;
|
||||
}
|
||||
|
||||
// Disable default fade so the JS-driven clip-path theme ripple isn't crossfaded.
|
||||
::view-transition-old(root),
|
||||
::view-transition-new(root) {
|
||||
animation: none;
|
||||
mix-blend-mode: normal;
|
||||
}
|
||||
|
||||
::view-transition-old(root) {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
::view-transition-new(root) {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
76
frontend/src/utils/themeRipple.ts
Normal file
76
frontend/src/utils/themeRipple.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { flushSync } from 'react-dom';
|
||||
|
||||
const RIPPLE_DURATION_MS = 500;
|
||||
const RIPPLE_EASING = 'ease-in-out';
|
||||
|
||||
type ViewTransition = { ready: Promise<void> };
|
||||
type DocumentWithVT = Document & {
|
||||
startViewTransition?: (callback: () => void) => ViewTransition;
|
||||
};
|
||||
|
||||
export type RippleOrigin = { x: number; y: number };
|
||||
|
||||
export function getRippleOrigin(
|
||||
element: Element | null | undefined,
|
||||
): RippleOrigin {
|
||||
const rect = element?.getBoundingClientRect();
|
||||
if (!rect) {
|
||||
return { x: window.innerWidth / 2, y: window.innerHeight / 2 };
|
||||
}
|
||||
return {
|
||||
x: rect.left + rect.width / 2,
|
||||
y: rect.top + rect.height / 2,
|
||||
};
|
||||
}
|
||||
|
||||
export function canAnimateThemeRipple(): boolean {
|
||||
const doc = document as DocumentWithVT;
|
||||
if (typeof doc.startViewTransition !== 'function') {
|
||||
return false;
|
||||
}
|
||||
return !window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
}
|
||||
|
||||
// Runs `applyChange` inside a View Transition and grows a circular clip-path
|
||||
// from `origin` until it covers the viewport. Callers should gate on
|
||||
// canAnimateThemeRipple() first; this is a safe no-animation fallback otherwise.
|
||||
export function runThemeRipple(
|
||||
origin: RippleOrigin,
|
||||
applyChange: () => void,
|
||||
): void {
|
||||
const doc = document as DocumentWithVT;
|
||||
if (!doc.startViewTransition) {
|
||||
applyChange();
|
||||
return;
|
||||
}
|
||||
|
||||
const transition = doc.startViewTransition(() => {
|
||||
flushSync(applyChange);
|
||||
});
|
||||
|
||||
const endRadius = distanceToFurthestCorner(origin);
|
||||
const fromCircle = `circle(0px at ${origin.x}px ${origin.y}px)`;
|
||||
const toCircle = `circle(${endRadius}px at ${origin.x}px ${origin.y}px)`;
|
||||
|
||||
transition.ready
|
||||
.then(() => {
|
||||
document.documentElement.animate(
|
||||
{ clipPath: [fromCircle, toCircle] },
|
||||
{
|
||||
duration: RIPPLE_DURATION_MS,
|
||||
easing: RIPPLE_EASING,
|
||||
pseudoElement: '::view-transition-new(root)',
|
||||
},
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
// Transition cancelled — applyChange has already run, so nothing to do.
|
||||
});
|
||||
}
|
||||
|
||||
function distanceToFurthestCorner({ x, y }: RippleOrigin): number {
|
||||
return Math.hypot(
|
||||
Math.max(x, window.innerWidth - x),
|
||||
Math.max(y, window.innerHeight - y),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user