diff --git a/packages/documentation-framework/components/example/example.js b/packages/documentation-framework/components/example/example.js
index ca9e08197e..e6f794ea66 100644
--- a/packages/documentation-framework/components/example/example.js
+++ b/packages/documentation-framework/components/example/example.js
@@ -1,4 +1,4 @@
-import React, { useContext, useEffect } from 'react';
+import React, { useContext, useEffect, useState, useCallback } from 'react';
import { useLocation } from '@reach/router';
import {
Button,
@@ -8,6 +8,10 @@ import {
debounce,
Label,
Switch,
+ Select,
+ SelectOption,
+ SelectList,
+ MenuToggle,
Tooltip,
Stack,
StackItem
@@ -19,6 +23,9 @@ import * as reactTableModule from '@patternfly/react-table';
import * as reactTableDeprecatedModule from '@patternfly/react-table/deprecated';
import { css } from '@patternfly/react-styles';
import { getParameters } from 'codesandbox/lib/api/define';
+import SunIcon from '@patternfly/react-icons/dist/esm/icons/sun-icon';
+import MoonIcon from '@patternfly/react-icons/dist/esm/icons/moon-icon';
+import DesktopIcon from '@patternfly/react-icons/dist/esm/icons/desktop-icon';
import { ExampleToolbar } from './exampleToolbar.jsx';
import { AutoLinkHeader } from '../autoLinkHeader/autoLinkHeader';
import {
@@ -32,9 +39,89 @@ import {
import { convertToReactComponent } from '@patternfly/ast-helpers';
import missingThumbnail from './missing-thumbnail.jpg';
import { RtlContext } from '../../layouts';
+import { useTheme } from '../../hooks/useTheme';
const errorComponent = (err) =>
{err.toString()};
+// Full-screen theme selector component using shared theme hook
+const FullScreenThemeSelector = () => {
+ const { themeMode, setThemeMode, THEME_MODES } = useTheme();
+ const [isThemeSelectOpen, setIsThemeSelectOpen] = useState(false);
+
+ const handleThemeChange = (_event, selectedMode) => {
+ setThemeMode(selectedMode);
+ setIsThemeSelectOpen(false);
+ };
+
+ const getThemeDisplayText = (mode) => {
+ switch (mode) {
+ case THEME_MODES.LIGHT:
+ return 'Light';
+ case THEME_MODES.DARK:
+ return 'Dark';
+ default:
+ return 'System';
+ }
+ };
+
+ const getThemeIcon = (mode) => {
+ switch (mode) {
+ case THEME_MODES.LIGHT:
+ return ;
+ case THEME_MODES.DARK:
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ return (
+
+ );
+};
+
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
@@ -199,21 +286,10 @@ export const Example = ({
{(hasDarkThemeSwitcher || hasRTLSwitcher) && (
- {hasDarkThemeSwitcher && (
-
- document
- .querySelector('html')
- .classList.toggle('pf-v6-theme-dark')
- }
- />
- )}
+ {hasDarkThemeSwitcher && }
{hasRTLSwitcher && (
{
+ const getStoredThemeMode = () => {
+ if (typeof window === 'undefined' || !window.localStorage) {
+ return null;
+ }
+ return localStorage.getItem(THEME_STORAGE_KEY);
+ };
+
+ const setStoredThemeMode = (mode) => {
+ if (typeof window === 'undefined' || !window.localStorage) {
+ return;
+ }
+ localStorage.setItem(THEME_STORAGE_KEY, mode);
+ };
+
+ const getResolvedTheme = (mode) => {
+ // SSR-safe check for window and matchMedia
+ if (typeof window === 'undefined' || !window.matchMedia) {
+ return 'light';
+ }
+
+ if (mode === THEME_MODES.SYSTEM) {
+ try {
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
+ } catch (error) {
+ // Fallback if matchMedia fails
+ console.warn('matchMedia not supported, defaulting to light theme');
+ return 'light';
+ }
+ }
+ return mode;
+ };
+
+ const updateThemeClass = (resolvedTheme) => {
+ if (typeof window === 'undefined' || !document) {
+ return;
+ }
+
+ const htmlElement = document.querySelector('html');
+ if (!htmlElement) {
+ return;
+ }
+
+ if (resolvedTheme === 'dark') {
+ htmlElement.classList.add(DARK_MODE_CLASS);
+ } else {
+ htmlElement.classList.remove(DARK_MODE_CLASS);
+ }
+ };
+
+ const [themeMode, setThemeModeState] = useState(() => {
+ const stored = getStoredThemeMode();
+ return stored && Object.values(THEME_MODES).includes(stored) ? stored : THEME_MODES.SYSTEM;
+ });
+
+ const [resolvedTheme, setResolvedTheme] = useState(() => getResolvedTheme(themeMode));
+
+ const setThemeMode = useCallback((newMode) => {
+ setThemeModeState(newMode);
+ setStoredThemeMode(newMode);
+
+ const newResolvedTheme = getResolvedTheme(newMode);
+ setResolvedTheme(newResolvedTheme);
+ updateThemeClass(newResolvedTheme);
+ }, []);
+
+ // Listen for system preference changes
+ useEffect(() => {
+ // Enhanced SSR-safe check
+ if (typeof window === 'undefined' || !window.matchMedia) {
+ return;
+ }
+
+ let mediaQuery;
+ try {
+ mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
+ } catch (error) {
+ console.warn('matchMedia not supported, skipping system theme detection');
+ return;
+ }
+
+ const handleSystemThemeChange = (e) => {
+ if (themeMode === THEME_MODES.SYSTEM) {
+ const newSystemTheme = e.matches ? 'dark' : 'light';
+ setResolvedTheme(newSystemTheme);
+ updateThemeClass(newSystemTheme);
+ }
+ };
+
+ // Check if addEventListener is available (some older browsers might not support it)
+ if (mediaQuery.addEventListener) {
+ mediaQuery.addEventListener('change', handleSystemThemeChange);
+ return () => {
+ mediaQuery.removeEventListener('change', handleSystemThemeChange);
+ };
+ } else if (mediaQuery.addListener) {
+ // Fallback for older browsers
+ mediaQuery.addListener(handleSystemThemeChange);
+ return () => {
+ mediaQuery.removeListener(handleSystemThemeChange);
+ };
+ }
+ }, [themeMode]);
+
+ // Initial theme application
+ useEffect(() => {
+ const initialResolvedTheme = getResolvedTheme(themeMode);
+ setResolvedTheme(initialResolvedTheme);
+ updateThemeClass(initialResolvedTheme);
+ }, [themeMode]);
+
+ return {
+ themeMode,
+ setThemeMode,
+ resolvedTheme,
+ THEME_MODES
+ };
+};
\ No newline at end of file
diff --git a/packages/documentation-framework/layouts/sideNavLayout/sideNavLayout.js b/packages/documentation-framework/layouts/sideNavLayout/sideNavLayout.js
index 8540701f4b..fb72522a80 100644
--- a/packages/documentation-framework/layouts/sideNavLayout/sideNavLayout.js
+++ b/packages/documentation-framework/layouts/sideNavLayout/sideNavLayout.js
@@ -23,17 +23,20 @@ import {
SkipToContent,
Switch,
SearchInput,
- ToggleGroup,
- ToggleGroupItem,
+ Select,
+ SelectOption,
+ SelectList,
MastheadLogo
} from '@patternfly/react-core';
import BarsIcon from '@patternfly/react-icons/dist/esm/icons/bars-icon';
import GithubIcon from '@patternfly/react-icons/dist/esm/icons/github-icon';
import MoonIcon from '@patternfly/react-icons/dist/esm/icons/moon-icon';
import SunIcon from '@patternfly/react-icons/dist/esm/icons/sun-icon';
+import DesktopIcon from '@patternfly/react-icons/dist/esm/icons/desktop-icon';
import { SideNav, TopNav, GdprBanner } from '../../components';
import staticVersions from '../../versions.json';
import { Footer } from '@patternfly/documentation-framework/components';
+import { useTheme } from '../../hooks/useTheme';
export const RtlContext = createContext(false);
@@ -46,8 +49,9 @@ const HeaderTools = ({
topNavItems,
isRTL,
setIsRTL,
- isDarkTheme,
- setIsDarkTheme
+ themeMode,
+ setThemeMode,
+ THEME_MODES
}) => {
const latestVersion = versions.Releases.find((version) => version.latest);
const previousReleases = Object.values(versions.Releases).filter((version) => !version.hidden && !version.latest);
@@ -55,6 +59,7 @@ const HeaderTools = ({
const [isDropdownOpen, setDropdownOpen] = useState(false);
const [searchValue, setSearchValue] = React.useState('');
const [isSearchExpanded, setIsSearchExpanded] = React.useState(false);
+ const [isThemeSelectOpen, setIsThemeSelectOpen] = useState(false);
const getDropdownItem = (version, isLatest = false) => (
@@ -70,10 +75,31 @@ const HeaderTools = ({
setIsSearchExpanded(!isSearchExpanded);
};
- const toggleDarkTheme = (_evt, selected) => {
- const darkThemeToggleClicked = !selected === isDarkTheme;
- document.querySelector('html').classList.toggle('pf-v6-theme-dark', darkThemeToggleClicked);
- setIsDarkTheme(darkThemeToggleClicked);
+ const handleThemeChange = (_event, selectedMode) => {
+ setThemeMode(selectedMode);
+ setIsThemeSelectOpen(false);
+ };
+
+ const getThemeDisplayText = (mode) => {
+ switch (mode) {
+ case THEME_MODES.LIGHT:
+ return 'Light';
+ case THEME_MODES.DARK:
+ return 'Dark';
+ default:
+ return 'System';
+ }
+ };
+
+ const getThemeIcon = (mode) => {
+ switch (mode) {
+ case THEME_MODES.LIGHT:
+ return ;
+ case THEME_MODES.DARK:
+ return ;
+ default:
+ return ;
+ }
};
useEffect(() => {
@@ -92,24 +118,6 @@ const HeaderTools = ({
)}
- {hasDarkThemeSwitcher && (
-
-
- }
- isSelected={!isDarkTheme}
- onChange={toggleDarkTheme}
- />
- }
- isSelected={isDarkTheme}
- onChange={toggleDarkTheme}
- />
-
-
- )}
{hasRTLSwitcher && (
+ {hasDarkThemeSwitcher && (
+
+
+
+ )}
{hasVersionSwitcher && (
{
if (typeof window === 'undefined') {
@@ -338,8 +394,9 @@ export const SideNavLayout = ({ children, groupedRoutes, navOpen: navOpenProp })
topNavItems={topNavItems}
isRTL={isRTL}
setIsRTL={setIsRTL}
- isDarkTheme={isDarkTheme}
- setIsDarkTheme={setIsDarkTheme}
+ themeMode={themeMode}
+ setThemeMode={setThemeMode}
+ THEME_MODES={THEME_MODES}
/>
)}
@@ -360,7 +417,7 @@ export const SideNavLayout = ({ children, groupedRoutes, navOpen: navOpenProp })
defaultManagedSidebarIsOpen={navOpenProp}
>
{children}
- {process.env.hasFooter && }
+ {process.env.hasFooter && }
{hasGdprBanner && }