chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:38:36 +08:00
commit 8e2a6eb840
10194 changed files with 1593658 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
export * from './lib/theme-panel';
export * from './lib/theme-resolver';
export * from './lib/theme-context';
export * from './lib/rankdir-context';
@@ -0,0 +1,43 @@
import {
createContext,
useContext,
useEffect,
useState,
ReactNode,
} from 'react';
import { RenderRankDir } from '@nx/graph';
export const localStorageRankDirKey = 'nx-dep-graph-rankdir';
interface RankDirContextValue {
rankDir: RenderRankDir;
setRankDir: (rankDir: RenderRankDir) => void;
}
const RankDirContext = createContext<RankDirContextValue | undefined>(
undefined
);
export function RankDirProvider({ children }: { children: ReactNode }) {
const [rankDir, setRankDir] = useState<RenderRankDir>(
(localStorage.getItem(localStorageRankDirKey) as RenderRankDir) || 'TB'
);
useEffect(() => {
localStorage.setItem(localStorageRankDirKey, rankDir);
}, [rankDir]);
return (
<RankDirContext.Provider value={{ rankDir, setRankDir }}>
{children}
</RankDirContext.Provider>
);
}
export function useRankDir() {
const context = useContext(RankDirContext);
if (context === undefined) {
throw new Error('useRankDir must be used within a RankDirProvider');
}
return context;
}
@@ -0,0 +1,55 @@
import {
createContext,
useContext,
useEffect,
useState,
ReactNode,
} from 'react';
import {
Theme,
themeResolver,
localStorageThemeKey,
getSystemTheme,
} from './theme-resolver';
import { RenderTheme } from '@nx/graph';
interface ThemeContextValue {
theme: Theme;
setTheme: (theme: Theme) => void;
resolvedTheme: RenderTheme;
}
const ThemeContext = createContext<ThemeContextValue | undefined>(undefined);
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<Theme>(
(localStorage.getItem(localStorageThemeKey) as Theme) || 'system'
);
const [resolvedTheme, setResolvedTheme] = useState<RenderTheme>(() =>
theme === 'system' ? getSystemTheme() : theme
);
useEffect(() => {
themeResolver(theme, setResolvedTheme);
}, [theme, setResolvedTheme]);
const handleSetTheme = (newTheme: Theme) => {
setTheme(newTheme);
};
return (
<ThemeContext.Provider
value={{ theme, setTheme: handleSetTheme, resolvedTheme }}
>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
@@ -0,0 +1,26 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { ThemePanel } from './theme-panel';
import { ThemeProvider } from './theme-context';
const meta: Meta<typeof ThemePanel> = {
component: ThemePanel,
decorators: [
(Story) => (
<ThemeProvider>
<div className="block h-auto w-auto border-2 bg-white px-8 py-4 dark:bg-slate-800">
<div className="flex justify-end">
<Story className="justify-items-end" />
</div>
</div>
</ThemeProvider>
),
],
title: 'ThemePanel',
};
export default meta;
type Story = StoryObj<typeof ThemePanel>;
export const Primary: Story = {
args: {},
};
@@ -0,0 +1,106 @@
import {
Menu,
MenuButton,
MenuItem,
MenuItems,
Transition,
} from '@headlessui/react';
import {
ComputerDesktopIcon,
MoonIcon,
SunIcon,
} from '@heroicons/react/24/outline';
import classNames from 'classnames';
import { Fragment } from 'react';
import { useTheme } from './theme-context';
export function ThemePanel(): JSX.Element {
const { theme, setTheme } = useTheme();
return (
<Menu as="div" className="relative inline-block text-left">
<div>
<MenuButton
className="inline-flex w-full justify-center rounded-md p-2 text-sm font-medium focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 dark:text-sky-500"
data-cy="theme-open-modal-button"
>
<span className="sr-only">Theme switcher</span>
{theme === 'system' && (
<ComputerDesktopIcon className="h-4 w-4" aria-hidden="true" />
)}
{theme === 'light' && (
<SunIcon className="h-4 w-4" aria-hidden="true" />
)}
{theme === 'dark' && (
<MoonIcon className="h-4 w-4" aria-hidden="true" />
)}
</MenuButton>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<MenuItems className="absolute right-0 z-50 mt-2 w-36 origin-top-right rounded-md bg-white text-slate-500 shadow-lg ring-1 ring-slate-900/10 ring-opacity-5 focus:outline-none dark:bg-slate-800 dark:text-slate-400 dark:ring-0">
<div className="px-1 py-1">
<MenuItem>
{({ focus }) => (
<button
data-cy="system-theme-button"
className={classNames(
theme === 'system' ? 'text-blue-500 dark:text-sky-500' : '',
focus ? 'bg-slate-50 dark:bg-slate-600/30' : '',
'group flex w-full items-center rounded-md px-2 py-2 text-sm'
)}
onClick={() => setTheme('system')}
>
<ComputerDesktopIcon
className="mr-2 h-4 w-4"
aria-hidden="true"
/>
System
</button>
)}
</MenuItem>
<MenuItem>
{({ focus }) => (
<button
data-cy="light-theme-button"
className={classNames(
theme === 'light' ? 'text-blue-500 dark:text-sky-500' : '',
focus ? 'bg-slate-50 dark:bg-slate-600/30' : '',
'group flex w-full items-center rounded-md px-2 py-2 text-sm'
)}
onClick={() => setTheme('light')}
>
<SunIcon className="mr-2 h-4 w-4" aria-hidden="true" />
Light
</button>
)}
</MenuItem>
<MenuItem>
{({ focus }) => (
<button
data-cy="dark-theme-button"
className={classNames(
theme === 'dark' ? 'text-blue-500 dark:text-sky-500' : '',
focus ? 'bg-slate-50 dark:bg-slate-600/30' : '',
'group flex w-full items-center rounded-md px-2 py-2 text-sm'
)}
onClick={() => setTheme('dark')}
>
<MoonIcon className="mr-2 h-4 w-4" aria-hidden="true" />
Dark
</button>
)}
</MenuItem>
</div>
</MenuItems>
</Transition>
</Menu>
);
}
@@ -0,0 +1,111 @@
import { getEnvironmentConfig } from '@nx/graph-shared';
import { RenderTheme } from '@nx/graph';
const htmlEl = document.documentElement;
export const localStorageThemeKey = 'nx-dep-graph-theme';
export type Theme = RenderTheme | 'system';
export let currentTheme: Theme;
export let mediaListener: ((ev: MediaQueryListEvent) => void) | undefined =
undefined;
export let vscodeDarkOberserver: MutationObserver | undefined = undefined;
// listen for (prefers-color-scheme: dark) changes
function mediaListenerFactory(
setResolvedTheme: (resolvedTheme: RenderTheme) => void
) {
return (ev: MediaQueryListEvent) => {
const resolver = ev.matches ? 'dark' : 'light';
toggleHtmlClass(resolver);
currentTheme = resolver;
setResolvedTheme(resolver);
};
}
// listen for body.vscode-dark changes
function vscodeDarkOberserverFactory(
setResolvedTheme: (resolvedTheme: RenderTheme) => void
) {
return new MutationObserver((mutations) => {
for (let mutation of mutations) {
if (mutation.type === 'attributes') {
const isVSCodeDark = document.body.classList.contains('vscode-dark');
const isVSCodeLight = document.body.classList.contains('vscode-light');
if (!isVSCodeDark && !isVSCodeLight) {
return;
}
const resolver = isVSCodeDark ? 'dark' : 'light';
toggleHtmlClass(resolver);
currentTheme = resolver;
setResolvedTheme(resolver);
}
}
});
}
function toggleHtmlClass(theme: Theme) {
if (theme === 'dark') {
htmlEl.classList.add('dark');
htmlEl.classList.remove('light');
} else {
htmlEl.classList.add('light');
htmlEl.classList.remove('dark');
}
}
export function getSystemTheme(): 'light' | 'dark' {
const isVSCodeDark = document.body.classList.contains('vscode-dark');
const isVSCodeLight = document.body.classList.contains('vscode-light');
if (isVSCodeDark || isVSCodeLight) {
return isVSCodeDark ? 'dark' : 'light';
}
// we don't want to use system theme in nx-console because it might conflict with the IDE theme
if (getEnvironmentConfig().environment === 'nx-console') {
return 'light';
}
const isDarkMedia = window.matchMedia('(prefers-color-scheme: dark)').matches;
return isDarkMedia || isVSCodeDark ? 'dark' : 'light';
}
export function themeResolver(
theme: Theme,
setResolvedTheme: (resolvedTheme: RenderTheme) => void
) {
if (!('matchMedia' in window)) {
return;
}
if (!mediaListener) {
mediaListener = mediaListenerFactory(setResolvedTheme);
}
if (!vscodeDarkOberserver) {
vscodeDarkOberserver = vscodeDarkOberserverFactory(setResolvedTheme);
}
const darkMedia = window.matchMedia('(prefers-color-scheme: dark)');
if (theme !== 'system') {
darkMedia.removeEventListener('change', mediaListener);
vscodeDarkOberserver.disconnect();
toggleHtmlClass(theme);
currentTheme = theme;
} else {
const resolver = getSystemTheme();
if (getEnvironmentConfig().environment !== 'nx-console') {
darkMedia.addEventListener('change', mediaListener);
}
vscodeDarkOberserver.observe(document.body, {
attributes: true,
attributeFilter: ['class'],
});
toggleHtmlClass(resolver);
currentTheme = resolver;
}
localStorage.setItem(localStorageThemeKey, theme);
setResolvedTheme(currentTheme);
return currentTheme;
}