@patternfly/documentation-framework 6.35.1 → 6.36.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,28 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## 6.36.1 (2026-02-19)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+ * **theme switch:** change unified theme class name ([#4934](https://github.com/patternfly/patternfly-org/issues/4934)) ([1caa470](https://github.com/patternfly/patternfly-org/commit/1caa470c123ecaacb36cb732eae844616717fc9b))
12
+
13
+
14
+
15
+
16
+
17
+ # 6.36.0 (2026-02-16)
18
+
19
+
20
+ ### Features
21
+
22
+ * **theme-switcher:** enhance theme switcher for theme development/testing ([#4929](https://github.com/patternfly/patternfly-org/issues/4929)) ([cf5a959](https://github.com/patternfly/patternfly-org/commit/cf5a95937b6a534f76e39cb4b2a19511842fbf2b))
23
+
24
+
25
+
26
+
27
+
6
28
  ## 6.35.1 (2026-02-11)
7
29
 
8
30
 
@@ -2,8 +2,6 @@ import React, { useState } from 'react';
2
2
  import {
3
3
  Select,
4
4
  SelectGroup,
5
- SelectList,
6
- SelectOption,
7
5
  MenuToggle,
8
6
  MenuSearch,
9
7
  MenuSearchInput,
@@ -11,12 +9,8 @@ import {
11
9
  ToggleGroupItem,
12
10
  Icon,
13
11
  Divider,
14
- Spinner,
15
- Label,
16
- Popover,
17
- Button
12
+ Spinner
18
13
  } from '@patternfly/react-core';
19
- import { HelpIcon, ExternalLinkAltIcon } from '@patternfly/react-icons';
20
14
  import { useTheme, THEME_TYPES } from '../../hooks/useTheme';
21
15
 
22
16
  const SunIcon = (
@@ -50,6 +44,14 @@ const DesktopIcon = (
50
44
  </svg>
51
45
  );
52
46
 
47
+ const ThemeVariantGroupLabel = () => {
48
+ return (
49
+ <div className="pf-v6-c-menu__group-title" id="theme-selector-variant-title">
50
+ Theme
51
+ </div>
52
+ );
53
+ };
54
+
53
55
  const ColorSchemeGroupLabel = () => {
54
56
  return (
55
57
  <div className="pf-v6-c-menu__group-title" id="theme-selector-color-scheme-title">
@@ -58,60 +60,34 @@ const ColorSchemeGroupLabel = () => {
58
60
  );
59
61
  };
60
62
 
61
- const HighContrastGroupLabel = () => {
62
- const [isPopoverOpen, setIsPopoverOpen] = useState(false);
63
-
63
+ const ContrastModeGroupLabel = () => {
64
64
  return (
65
- <div className="pf-v6-c-menu__group-title">
66
- High contrast{' '}
67
- <Popover
68
- onClick={(e) => e.stopPropagation()}
69
- headerContent={'Under development'}
70
- headerComponent="h1"
71
- bodyContent={
72
- 'We are still working to add high contrast support across all PatternFly components and extensions. This beta allows you to preview our progress.'
73
- }
74
- footerContent={
75
- <Button
76
- icon={<ExternalLinkAltIcon />}
77
- component="a"
78
- isInline
79
- variant="link"
80
- href="/design-foundations/theming"
81
- target="_blank"
82
- >
83
- Learn more
84
- </Button>
85
- }
86
- aria-label="More info about high contrast"
87
- appendTo={() => document.body}
88
- >
89
- <Button variant="plain" hasNoPadding icon={<HelpIcon />} aria-label="High contrast help" />
90
- </Popover>{' '}
91
- &nbsp;
92
- <Label color="blue" isCompact>
93
- Beta
94
- </Label>
65
+ <div className="pf-v6-c-menu__group-title" id="theme-selector-contrast-title">
66
+ Contrast mode
95
67
  </div>
96
68
  );
97
69
  };
98
70
 
99
71
  export const ThemeSelector = ({ id }) => {
72
+ const { mode: themeVariant, setMode: setThemeVariant, modes: themeVariantModes } = useTheme(THEME_TYPES.THEME_VARIANT);
100
73
  const { mode: themeMode, setMode: setThemeMode, modes: colorModes } = useTheme(THEME_TYPES.COLOR);
101
74
  const {
102
- mode: highContrastMode,
103
- setMode: setHighContrastMode,
104
- modes: highContrastModes
105
- } = useTheme(THEME_TYPES.HIGH_CONTRAST);
75
+ mode: contrastMode,
76
+ setMode: setContrastMode,
77
+ modes: contrastModes
78
+ } = useTheme(THEME_TYPES.CONTRAST);
106
79
  const [isThemeSelectOpen, setIsThemeSelectOpen] = useState(false);
107
80
 
108
- const handleThemeChange = (_event, selectedMode) => {
109
- setThemeMode(selectedMode);
110
- setIsThemeSelectOpen(false);
81
+ const handleThemeVariantChange = (evt) => {
82
+ setThemeVariant(evt.currentTarget.id);
111
83
  };
112
84
 
113
- const handleHighContrastThemeSelection = (evt) => {
114
- setHighContrastMode(evt.currentTarget.id);
85
+ const handleThemeChange = (evt) => {
86
+ setThemeMode(evt.currentTarget.id);
87
+ };
88
+
89
+ const handleContrastModeChange = (evt) => {
90
+ setContrastMode(evt.currentTarget.id);
115
91
  };
116
92
 
117
93
  const getThemeDisplayText = (mode) => {
@@ -126,6 +102,9 @@ export const ThemeSelector = ({ id }) => {
126
102
  };
127
103
 
128
104
  const getThemeIcon = (mode) => {
105
+ if (!colorModes) {
106
+ return <Spinner size="sm" />;
107
+ }
129
108
  switch (mode) {
130
109
  case colorModes.LIGHT:
131
110
  return SunIcon;
@@ -134,7 +113,7 @@ export const ThemeSelector = ({ id }) => {
134
113
  case colorModes.SYSTEM:
135
114
  return DesktopIcon;
136
115
  default:
137
- return <Spinner size="sm" />;
116
+ return DesktopIcon; // Default to system icon
138
117
  }
139
118
  };
140
119
 
@@ -142,8 +121,6 @@ export const ThemeSelector = ({ id }) => {
142
121
  <Select
143
122
  id={id}
144
123
  isOpen={isThemeSelectOpen}
145
- selected={themeMode}
146
- onSelect={handleThemeChange}
147
124
  onOpenChange={(isOpen) => setIsThemeSelectOpen(isOpen)}
148
125
  toggle={(toggleRef) => (
149
126
  <MenuToggle
@@ -162,50 +139,86 @@ export const ThemeSelector = ({ id }) => {
162
139
  preventOverflow: true
163
140
  }}
164
141
  >
142
+ <SelectGroup label={ThemeVariantGroupLabel}>
143
+ <MenuSearch>
144
+ <MenuSearchInput>
145
+ <ToggleGroup aria-labelledby="theme-selector-variant-title">
146
+ <ToggleGroupItem
147
+ text="Default"
148
+ buttonId={themeVariantModes.DEFAULT}
149
+ isSelected={themeVariant === themeVariantModes.DEFAULT}
150
+ onChange={handleThemeVariantChange}
151
+ />
152
+ <ToggleGroupItem
153
+ text="Unified"
154
+ buttonId={themeVariantModes.UNIFIED}
155
+ isSelected={themeVariant === themeVariantModes.UNIFIED}
156
+ onChange={handleThemeVariantChange}
157
+ />
158
+ </ToggleGroup>
159
+ </MenuSearchInput>
160
+ </MenuSearch>
161
+ </SelectGroup>
162
+ <Divider />
165
163
  <SelectGroup label={ColorSchemeGroupLabel}>
166
- <SelectList aria-labelledby="theme-selector-color-scheme-title">
167
- <SelectOption value={colorModes.SYSTEM} icon={DesktopIcon} description="Follow system preference">
168
- System
169
- </SelectOption>
170
- <SelectOption value={colorModes.LIGHT} icon={SunIcon} description="Always use light mode">
171
- Light
172
- </SelectOption>
173
- <SelectOption value={colorModes.DARK} icon={MoonIcon} description="Always use dark mode">
174
- Dark
175
- </SelectOption>
176
- </SelectList>
164
+ <MenuSearch>
165
+ <MenuSearchInput>
166
+ <ToggleGroup aria-labelledby="theme-selector-color-scheme-title">
167
+ <ToggleGroupItem
168
+ text="System"
169
+ buttonId={colorModes.SYSTEM}
170
+ isSelected={themeMode === colorModes.SYSTEM}
171
+ onChange={handleThemeChange}
172
+ />
173
+ <ToggleGroupItem
174
+ text="Light"
175
+ buttonId={colorModes.LIGHT}
176
+ isSelected={themeMode === colorModes.LIGHT}
177
+ onChange={handleThemeChange}
178
+ />
179
+ <ToggleGroupItem
180
+ text="Dark"
181
+ buttonId={colorModes.DARK}
182
+ isSelected={themeMode === colorModes.DARK}
183
+ onChange={handleThemeChange}
184
+ />
185
+ </ToggleGroup>
186
+ </MenuSearchInput>
187
+ </MenuSearch>
188
+ </SelectGroup>
189
+ <Divider />
190
+ <SelectGroup label={ContrastModeGroupLabel}>
191
+ <MenuSearch>
192
+ <MenuSearchInput>
193
+ <ToggleGroup aria-labelledby="theme-selector-contrast-title">
194
+ <ToggleGroupItem
195
+ text="System"
196
+ buttonId={contrastModes.SYSTEM}
197
+ isSelected={contrastMode === contrastModes.SYSTEM}
198
+ onChange={handleContrastModeChange}
199
+ />
200
+ <ToggleGroupItem
201
+ text="Default"
202
+ buttonId={contrastModes.DEFAULT}
203
+ isSelected={contrastMode === contrastModes.DEFAULT}
204
+ onChange={handleContrastModeChange}
205
+ />
206
+ <ToggleGroupItem
207
+ text="High contrast"
208
+ buttonId={contrastModes.HIGH_CONTRAST}
209
+ isSelected={contrastMode === contrastModes.HIGH_CONTRAST}
210
+ onChange={handleContrastModeChange}
211
+ />
212
+ <ToggleGroupItem
213
+ text="Glass"
214
+ buttonId={contrastModes.GLASS}
215
+ isSelected={contrastMode === contrastModes.GLASS}
216
+ onChange={handleContrastModeChange}
217
+ />
218
+ </ToggleGroup>
219
+ </MenuSearchInput>
220
+ </MenuSearch>
177
221
  </SelectGroup>
178
- {process.env.hasHighContrastSwitcher && (
179
- <>
180
- <Divider />
181
- <SelectGroup label={HighContrastGroupLabel}>
182
- <MenuSearch>
183
- <MenuSearchInput>
184
- <ToggleGroup aria-label="High contrast theme switcher">
185
- <ToggleGroupItem
186
- text="System"
187
- buttonId={highContrastModes.SYSTEM}
188
- isSelected={highContrastMode === highContrastModes.SYSTEM}
189
- onChange={handleHighContrastThemeSelection}
190
- />
191
- <ToggleGroupItem
192
- text="On"
193
- buttonId={highContrastModes.ON}
194
- isSelected={highContrastMode === highContrastModes.ON}
195
- onChange={handleHighContrastThemeSelection}
196
- />
197
- <ToggleGroupItem
198
- text="Off"
199
- buttonId={highContrastModes.OFF}
200
- isSelected={highContrastMode === highContrastModes.OFF}
201
- onChange={handleHighContrastThemeSelection}
202
- />
203
- </ToggleGroup>
204
- </MenuSearchInput>
205
- </MenuSearch>
206
- </SelectGroup>
207
- </>
208
- )}
209
222
  </Select>
210
223
  );
211
224
  };
package/hooks/useTheme.js CHANGED
@@ -6,15 +6,22 @@ const COLOR_MODES = {
6
6
  DARK: 'dark'
7
7
  };
8
8
 
9
- const HIGH_CONTRAST_MODES = {
10
- SYSTEM: 'high-contrast-system',
11
- ON: 'high-contrast-on',
12
- OFF: 'high-contrast-off'
9
+ const CONTRAST_MODES = {
10
+ SYSTEM: 'contrast-system',
11
+ DEFAULT: 'contrast-default',
12
+ HIGH_CONTRAST: 'contrast-high',
13
+ GLASS: 'contrast-glass'
14
+ };
15
+
16
+ const THEME_VARIANT_MODES = {
17
+ DEFAULT: 'theme-default',
18
+ UNIFIED: 'theme-redhat'
13
19
  };
14
20
 
15
21
  export const THEME_TYPES = {
16
22
  COLOR: 'color',
17
- HIGH_CONTRAST: 'high-contrast'
23
+ CONTRAST: 'contrast',
24
+ THEME_VARIANT: 'theme-variant'
18
25
  };
19
26
 
20
27
  class ThemeManager {
@@ -61,37 +68,105 @@ class ThemeManager {
61
68
  return this.defaultMode;
62
69
  }
63
70
 
64
- addClass() {
71
+ getHtmlElement() {
65
72
  if (!this.isBrowser) {
66
- return;
73
+ return null;
74
+ }
75
+ return document.querySelector('html');
76
+ }
77
+
78
+ addClass() {
79
+ const htmlElement = this.getHtmlElement();
80
+ if (htmlElement && !htmlElement.classList.contains(this.cssClass)) {
81
+ htmlElement.classList.add(this.cssClass);
67
82
  }
68
- document.querySelector('html').classList.add(this.cssClass);
69
83
  }
70
84
 
71
85
  removeClass() {
86
+ const htmlElement = this.getHtmlElement();
87
+ if (htmlElement && htmlElement.classList.contains(this.cssClass)) {
88
+ htmlElement.classList.remove(this.cssClass);
89
+ }
90
+ }
91
+
92
+ updateClass() {
72
93
  if (!this.isBrowser) {
73
94
  return;
74
95
  }
75
- document.querySelector('html').classList.remove(this.cssClass);
96
+
97
+ // ALWAYS read from localStorage to ensure we have the correct mode for THIS theme
98
+ const storedMode = this.getStoredValue();
99
+
100
+ // Validate that the stored mode is valid for this theme
101
+ const validModes = Object.values(this.modes);
102
+ if (!validModes.includes(storedMode)) {
103
+ console.error(`[${this.storageKey}] Invalid stored mode "${storedMode}". Valid modes:`, validModes);
104
+ return;
105
+ }
106
+
107
+ const shouldHaveClass = storedMode === this.modes.SYSTEM
108
+ ? this.resolve() === this.classEnabledMode
109
+ : storedMode === this.classEnabledMode;
110
+
111
+ if (shouldHaveClass) {
112
+ this.addClass();
113
+ } else {
114
+ this.removeClass();
115
+ }
76
116
  }
117
+ }
77
118
 
78
- updateClass(mode) {
119
+ class ContrastThemeManager extends ThemeManager {
120
+ constructor({ storageKey, modes, defaultMode, mediaQueryString }) {
121
+ super({
122
+ storageKey,
123
+ modes,
124
+ defaultMode,
125
+ cssClass: 'pf-v6-theme-high-contrast',
126
+ classEnabledMode: modes.HIGH_CONTRAST,
127
+ mediaQueryString
128
+ });
129
+ this.glassClass = 'pf-v6-theme-glass';
130
+ }
131
+
132
+ updateClass() {
79
133
  if (!this.isBrowser) {
80
134
  return;
81
135
  }
82
136
 
83
- if (mode === this.modes.SYSTEM) {
84
- if (this.resolve() === this.classEnabledMode) {
85
- this.addClass();
86
- } else {
87
- this.removeClass();
88
- }
89
- } else {
90
- if (mode === this.classEnabledMode) {
91
- this.addClass();
92
- } else {
93
- this.removeClass();
94
- }
137
+ const htmlElement = this.getHtmlElement();
138
+ if (!htmlElement) {
139
+ return;
140
+ }
141
+
142
+ // ALWAYS read from localStorage to ensure we have the correct mode for THIS theme
143
+ const storedMode = this.getStoredValue();
144
+
145
+ // Determine which class should be applied based on stored mode
146
+ let shouldHaveHighContrast = false;
147
+ let shouldHaveGlass = false;
148
+
149
+ if (storedMode === this.modes.SYSTEM) {
150
+ shouldHaveHighContrast = window.matchMedia(this.mediaQueryString).matches;
151
+ } else if (storedMode === this.modes.HIGH_CONTRAST) {
152
+ shouldHaveHighContrast = true;
153
+ } else if (storedMode === this.modes.GLASS) {
154
+ shouldHaveGlass = true;
155
+ }
156
+ // DEFAULT mode: both false
157
+
158
+ // Apply high contrast class
159
+ if (shouldHaveHighContrast && !htmlElement.classList.contains(this.cssClass)) {
160
+ htmlElement.classList.add(this.cssClass);
161
+ } else if (!shouldHaveHighContrast && htmlElement.classList.contains(this.cssClass)) {
162
+ htmlElement.classList.remove(this.cssClass);
163
+ }
164
+
165
+ // Apply glass class
166
+ if (shouldHaveGlass && !htmlElement.classList.contains(this.glassClass)) {
167
+ htmlElement.classList.add(this.glassClass);
168
+ } else if (!shouldHaveGlass && htmlElement.classList.contains(this.glassClass)) {
169
+ htmlElement.classList.remove(this.glassClass);
95
170
  }
96
171
  }
97
172
  }
@@ -111,26 +186,34 @@ const colorThemeManager = new ThemeManager({
111
186
  mediaQueryString: '(prefers-color-scheme: dark)'
112
187
  });
113
188
 
114
- const highContrastThemeManager = new ThemeManager({
115
- storageKey: 'high-contrast-preference',
116
- modes: HIGH_CONTRAST_MODES,
117
- defaultMode: HIGH_CONTRAST_MODES.SYSTEM,
118
- cssClass: 'pf-v6-theme-high-contrast',
119
- classEnabledMode: HIGH_CONTRAST_MODES.ON,
189
+ const themeVariantManager = new ThemeManager({
190
+ storageKey: 'theme-variant-preference',
191
+ modes: THEME_VARIANT_MODES,
192
+ defaultMode: THEME_VARIANT_MODES.DEFAULT,
193
+ cssClass: 'pf-v6-theme-redhat',
194
+ classEnabledMode: THEME_VARIANT_MODES.UNIFIED,
195
+ mediaQueryString: '(prefers-color-scheme: dark)' // Not used for variant, but required
196
+ });
197
+
198
+ const contrastThemeManager = new ContrastThemeManager({
199
+ storageKey: 'contrast-preference',
200
+ modes: CONTRAST_MODES,
201
+ defaultMode: CONTRAST_MODES.SYSTEM,
120
202
  mediaQueryString: '(prefers-contrast: more)'
121
203
  });
122
204
 
123
205
  registerThemeManager(THEME_TYPES.COLOR, colorThemeManager);
124
- registerThemeManager(THEME_TYPES.HIGH_CONTRAST, highContrastThemeManager);
206
+ registerThemeManager(THEME_TYPES.THEME_VARIANT, themeVariantManager);
207
+ registerThemeManager(THEME_TYPES.CONTRAST, contrastThemeManager);
125
208
 
126
209
  /**
127
210
  * Unified theme hook that accepts a theme type parameter
128
- * @param {string} themeType - The type of theme to manage (THEME_TYPES.COLOR, THEME_TYPES.HIGH_CONTRAST, instantiate and register new themes above as needed)
211
+ * @param {string} themeType - The type of theme to manage (THEME_TYPES.COLOR, THEME_TYPES.CONTRAST, THEME_TYPES.THEME_VARIANT)
129
212
  * @returns {Object} Theme state and controls specific to the theme type
130
213
  */
131
214
  export const useTheme = (themeType) => {
132
215
  if (!themeType) {
133
- throw new Error('useTheme requires a theme type parameter. Use THEME_TYPES.COLOR or THEME_TYPES.HIGH_CONTRAST');
216
+ throw new Error('useTheme requires a theme type parameter. Use THEME_TYPES.COLOR, THEME_TYPES.CONTRAST, or THEME_TYPES.THEME_VARIANT');
134
217
  }
135
218
 
136
219
  const theme = themeRegistry.get(themeType);
@@ -143,16 +226,34 @@ export const useTheme = (themeType) => {
143
226
  const [resolvedTheme, setResolvedTheme] = useState(theme.resolve());
144
227
 
145
228
  useEffect(() => {
229
+ // Verify mode is valid for this theme
230
+ const validModes = Object.values(theme.modes);
231
+ if (!validModes.includes(mode)) {
232
+ console.error(`Invalid mode "${mode}" for theme ${theme.storageKey}. Valid modes:`, validModes);
233
+ return;
234
+ }
235
+
146
236
  theme.setStoredValue(mode);
147
- theme.updateClass(mode);
148
- }, [theme, mode, resolvedTheme]);
237
+ theme.updateClass();
238
+ }, [theme, mode]);
149
239
 
150
- const handlePreferenceChange = () => {
151
- setResolvedTheme(theme.resolve());
152
- };
153
- const mediaQuery = theme.getMediaQuery();
240
+ useEffect(() => {
241
+ // Only update class when system preference changes AND mode is SYSTEM
242
+ if (mode === theme.modes.SYSTEM) {
243
+ theme.updateClass();
244
+ }
245
+ }, [theme, mode, resolvedTheme]);
154
246
 
155
247
  useEffect(() => {
248
+ const handlePreferenceChange = () => {
249
+ setResolvedTheme(theme.resolve());
250
+ };
251
+
252
+ const mediaQuery = theme.getMediaQuery();
253
+
254
+ if (!mediaQuery) {
255
+ return;
256
+ }
156
257
  if (mediaQuery.addEventListener) {
157
258
  mediaQuery.addEventListener('change', handlePreferenceChange);
158
259
  return () => {
@@ -164,7 +265,7 @@ export const useTheme = (themeType) => {
164
265
  mediaQuery.removeListener(handlePreferenceChange);
165
266
  };
166
267
  }
167
- }, [mediaQuery]);
268
+ }, []);
168
269
 
169
270
  return {
170
271
  mode,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@patternfly/documentation-framework",
3
3
  "description": "A framework to build documentation for PatternFly.",
4
- "version": "6.35.1",
4
+ "version": "6.36.1",
5
5
  "author": "Red Hat",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -12,7 +12,7 @@
12
12
  "@babel/preset-env": "7.27.1",
13
13
  "@babel/preset-react": "^7.24.1",
14
14
  "@mdx-js/util": "1.6.16",
15
- "@patternfly/ast-helpers": "^1.4.0-alpha.341",
15
+ "@patternfly/ast-helpers": "^1.4.0-alpha.343",
16
16
  "@reach/router": "npm:@gatsbyjs/reach-router@1.3.9",
17
17
  "@rspack/core": "^1.5.6",
18
18
  "@rspack/dev-server": "^1.1.4",
@@ -92,5 +92,5 @@
92
92
  "http-cache-semantics": ">=4.1.1",
93
93
  "nanoid": "3.3.8"
94
94
  },
95
- "gitHead": "89b2ca9cc6eb24548ce7baaf58b4a2fe25a547c3"
95
+ "gitHead": "c51df5b7c4db53d8270141f4e0a1dc8f39cd0ba1"
96
96
  }