@patternfly/documentation-framework 6.18.6 → 6.19.0
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 +22 -0
- package/components/example/example.js +50 -167
- package/components/index.js +1 -0
- package/components/themeSelector/themeSelector.js +148 -0
- package/hooks/useTheme.js +135 -98
- package/layouts/sideNavLayout/sideNavLayout.js +9 -90
- package/package.json +3 -3
- package/scripts/md/typecheck.js +2 -2
- package/scripts/webpack/webpack.base.config.js +2 -2
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.19.0 (2025-08-26)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **docs-framework:** add high contrast theme switcher ([#4749](https://github.com/patternfly/patternfly-org/issues/4749)) ([1a24fb2](https://github.com/patternfly/patternfly-org/commit/1a24fb234bbaee2d569c16b4b1a90609fe006ddf))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## 6.18.7 (2025-08-19)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Bug Fixes
|
|
21
|
+
|
|
22
|
+
* fix glob reference ([#4742](https://github.com/patternfly/patternfly-org/issues/4742)) ([bb16c5e](https://github.com/patternfly/patternfly-org/commit/bb16c5e1757a07a97b293826c583a4ed9ab71feb))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
6
28
|
## 6.18.6 (2025-08-19)
|
|
7
29
|
|
|
8
30
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useContext, useEffect
|
|
1
|
+
import React, { useContext, useEffect } from 'react';
|
|
2
2
|
import { useLocation } from '@reach/router';
|
|
3
3
|
import {
|
|
4
4
|
Button,
|
|
@@ -6,16 +6,11 @@ import {
|
|
|
6
6
|
Flex,
|
|
7
7
|
CodeBlockCode,
|
|
8
8
|
debounce,
|
|
9
|
-
Icon,
|
|
10
9
|
Label,
|
|
11
10
|
Switch,
|
|
12
|
-
Select,
|
|
13
|
-
SelectOption,
|
|
14
|
-
SelectList,
|
|
15
|
-
MenuToggle,
|
|
16
11
|
Tooltip,
|
|
17
12
|
Stack,
|
|
18
|
-
StackItem
|
|
13
|
+
StackItem,
|
|
19
14
|
} from '@patternfly/react-core';
|
|
20
15
|
import * as reactCoreModule from '@patternfly/react-core';
|
|
21
16
|
import * as reactCoreNextModule from '@patternfly/react-core/next';
|
|
@@ -24,9 +19,6 @@ import * as reactTableModule from '@patternfly/react-table';
|
|
|
24
19
|
import * as reactTableDeprecatedModule from '@patternfly/react-table/deprecated';
|
|
25
20
|
import { css } from '@patternfly/react-styles';
|
|
26
21
|
import { getParameters } from 'codesandbox/lib/api/define';
|
|
27
|
-
const SunIcon = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" className="pf-v6-svg" fill="var(--pf-t--global--icon--color--regular)"><path d="M16 25c-4.963 0-9-4.038-9-9s4.037-9 9-9 9 4.038 9 9-4.037 9-9 9Zm0-16c-3.86 0-7 3.14-7 7s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7Zm0-4a1 1 0 0 1-1-1V1a1 1 0 1 1 2 0v3a1 1 0 0 1-1 1Zm0 27a1 1 0 0 1-1-1v-3a1 1 0 1 1 2 0v3a1 1 0 0 1-1 1ZM4 17H1a1 1 0 1 1 0-2h3a1 1 0 1 1 0 2Zm27 0h-3a1 1 0 1 1 0-2h3a1 1 0 1 1 0 2ZM5.394 27.606a1 1 0 0 1-.707-1.707l2.12-2.12a1 1 0 1 1 1.415 1.413L6.1 27.313a.997.997 0 0 1-.707.293ZM24.485 8.515a1 1 0 0 1-.707-1.707L25.9 4.686a1 1 0 1 1 1.415 1.415l-2.122 2.12a.997.997 0 0 1-.707.294Zm-16.97 0a.997.997 0 0 1-.707-.293L4.686 6.1a1 1 0 1 1 1.415-1.415l2.12 2.122a1 1 0 0 1-.706 1.707Zm19.091 19.091a.997.997 0 0 1-.707-.293l-2.12-2.12a1 1 0 1 1 1.413-1.415l2.122 2.121a1 1 0 0 1-.707 1.707Z"></path></svg>;
|
|
28
|
-
const MoonIcon = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" className="pf-v6-svg" fill="var(--pf-t--global--icon--color--regular)"><path d="M16.457 30C8.485 30 2 23.515 2 15.543c0-5.93 3.715-11.345 9.243-13.476a.999.999 0 0 1 1.289 1.3 12.185 12.185 0 0 0-.843 4.487c0 6.869 5.588 12.457 12.457 12.457 1.56 0 3.07-.284 4.487-.844a.998.998 0 0 1 1.3 1.29C27.802 26.285 22.387 30 16.456 30ZM9.992 4.904C6.338 7.134 4 11.177 4 15.544 4 22.412 9.588 28 16.457 28c4.367 0 8.41-2.338 10.639-5.992a14.39 14.39 0 0 1-2.95.302c-7.971 0-14.457-6.485-14.457-14.456 0-1.003.102-1.989.303-2.95Z"></path></svg>;
|
|
29
|
-
const DesktopIcon = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" className="pf-v6-svg" fill="var(--pf-t--global--icon--color--regular)"><path d="M23.94 16a1 1 0 0 1-.992-.876 6.957 6.957 0 0 0-6.069-6.062 1 1 0 1 1 .242-1.985 8.953 8.953 0 0 1 7.812 7.8A.999.999 0 0 1 23.94 16ZM16 5a1 1 0 0 1-1-1V1a1 1 0 1 1 2 0v3a1 1 0 0 1-1 1Zm0 27a1 1 0 0 1-1-1v-3a1 1 0 1 1 2 0v3a1 1 0 0 1-1 1ZM4 17H1a1 1 0 1 1 0-2h3a1 1 0 1 1 0 2Zm27 0h-3a1 1 0 1 1 0-2h3a1 1 0 1 1 0 2ZM5.394 27.606a1 1 0 0 1-.707-1.707l2.12-2.12a1 1 0 1 1 1.415 1.413L6.1 27.313a.997.997 0 0 1-.707.293ZM24.485 8.515a1 1 0 0 1-.707-1.707L25.9 4.686a1 1 0 1 1 1.415 1.415l-2.122 2.12a.997.997 0 0 1-.707.294Zm-16.97 0a.997.997 0 0 1-.707-.293L4.686 6.1a1 1 0 1 1 1.415-1.415l2.12 2.122a1 1 0 0 1-.706 1.707Zm19.091 19.091a.997.997 0 0 1-.707-.293l-2.12-2.12a1 1 0 1 1 1.413-1.415l2.122 2.121a1 1 0 0 1-.707 1.707ZM16 24.875c-4.894 0-8.875-3.981-8.875-8.875a8.879 8.879 0 0 1 5.227-8.088.876.876 0 0 1 1.153 1.163 6.945 6.945 0 0 0-.63 2.925A7.133 7.133 0 0 0 20 19.125a6.948 6.948 0 0 0 2.925-.63.876.876 0 0 1 1.163 1.154A8.88 8.88 0 0 1 16 24.875Zm-4.785-14.153A7.135 7.135 0 0 0 8.875 16 7.133 7.133 0 0 0 16 23.125a7.13 7.13 0 0 0 5.278-2.34c-.419.06-.845.09-1.278.09-4.894 0-8.875-3.981-8.875-8.875 0-.433.03-.86.09-1.278Z"></path></svg>;
|
|
30
22
|
import { ExampleToolbar } from './exampleToolbar.jsx';
|
|
31
23
|
import { AutoLinkHeader } from '../autoLinkHeader/autoLinkHeader';
|
|
32
24
|
import {
|
|
@@ -35,92 +27,15 @@ import {
|
|
|
35
27
|
getReactParams,
|
|
36
28
|
getExampleClassName,
|
|
37
29
|
getExampleId,
|
|
38
|
-
liveCodeTypes
|
|
30
|
+
liveCodeTypes
|
|
39
31
|
} from '../../helpers';
|
|
40
32
|
import { convertToReactComponent } from '@patternfly/ast-helpers';
|
|
41
33
|
import missingThumbnail from './missing-thumbnail.jpg';
|
|
42
34
|
import { RtlContext } from '../../layouts';
|
|
43
|
-
import {
|
|
35
|
+
import { ThemeSelector } from '../themeSelector/themeSelector';
|
|
44
36
|
|
|
45
37
|
const errorComponent = (err) => <pre>{err.toString()}</pre>;
|
|
46
38
|
|
|
47
|
-
// Full-screen theme selector component using shared theme hook
|
|
48
|
-
const FullScreenThemeSelector = () => {
|
|
49
|
-
const { themeMode, setThemeMode, THEME_MODES } = useTheme();
|
|
50
|
-
const [isThemeSelectOpen, setIsThemeSelectOpen] = useState(false);
|
|
51
|
-
|
|
52
|
-
const handleThemeChange = (_event, selectedMode) => {
|
|
53
|
-
setThemeMode(selectedMode);
|
|
54
|
-
setIsThemeSelectOpen(false);
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
const getThemeDisplayText = (mode) => {
|
|
58
|
-
switch (mode) {
|
|
59
|
-
case THEME_MODES.LIGHT:
|
|
60
|
-
return 'Light';
|
|
61
|
-
case THEME_MODES.DARK:
|
|
62
|
-
return 'Dark';
|
|
63
|
-
default:
|
|
64
|
-
return 'System';
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const getThemeIcon = (mode) => {
|
|
69
|
-
switch (mode) {
|
|
70
|
-
case THEME_MODES.LIGHT:
|
|
71
|
-
return SunIcon;
|
|
72
|
-
case THEME_MODES.DARK:
|
|
73
|
-
return MoonIcon;
|
|
74
|
-
default:
|
|
75
|
-
return DesktopIcon;
|
|
76
|
-
}
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
return (
|
|
80
|
-
<Select
|
|
81
|
-
id="ws-example-theme-select"
|
|
82
|
-
isOpen={isThemeSelectOpen}
|
|
83
|
-
selected={themeMode}
|
|
84
|
-
onSelect={handleThemeChange}
|
|
85
|
-
onOpenChange={(isOpen) => setIsThemeSelectOpen(isOpen)}
|
|
86
|
-
toggle={(toggleRef) => (
|
|
87
|
-
<MenuToggle
|
|
88
|
-
ref={toggleRef}
|
|
89
|
-
onClick={() => setIsThemeSelectOpen(!isThemeSelectOpen)}
|
|
90
|
-
isExpanded={isThemeSelectOpen}
|
|
91
|
-
icon={<Icon size="lg">{getThemeIcon(themeMode)}</Icon>}
|
|
92
|
-
aria-label={`Theme selection, current: ${getThemeDisplayText(themeMode)}`}
|
|
93
|
-
/>
|
|
94
|
-
)}
|
|
95
|
-
shouldFocusToggleOnSelect
|
|
96
|
-
>
|
|
97
|
-
<SelectList>
|
|
98
|
-
<SelectOption
|
|
99
|
-
value={THEME_MODES.SYSTEM}
|
|
100
|
-
icon={DesktopIcon}
|
|
101
|
-
description="Follow system preference"
|
|
102
|
-
>
|
|
103
|
-
System
|
|
104
|
-
</SelectOption>
|
|
105
|
-
<SelectOption
|
|
106
|
-
value={THEME_MODES.LIGHT}
|
|
107
|
-
icon={SunIcon}
|
|
108
|
-
description="Always use light mode"
|
|
109
|
-
>
|
|
110
|
-
Light
|
|
111
|
-
</SelectOption>
|
|
112
|
-
<SelectOption
|
|
113
|
-
value={THEME_MODES.DARK}
|
|
114
|
-
icon={MoonIcon}
|
|
115
|
-
description="Always use dark mode"
|
|
116
|
-
>
|
|
117
|
-
Dark
|
|
118
|
-
</SelectOption>
|
|
119
|
-
</SelectList>
|
|
120
|
-
</Select>
|
|
121
|
-
);
|
|
122
|
-
};
|
|
123
|
-
|
|
124
39
|
class ErrorBoundary extends React.Component {
|
|
125
40
|
constructor(props) {
|
|
126
41
|
super(props);
|
|
@@ -131,7 +46,7 @@ class ErrorBoundary extends React.Component {
|
|
|
131
46
|
errorInfo._suppressLogging = true;
|
|
132
47
|
this.setState({
|
|
133
48
|
error: error,
|
|
134
|
-
errorInfo: errorInfo
|
|
49
|
+
errorInfo: errorInfo
|
|
135
50
|
});
|
|
136
51
|
}
|
|
137
52
|
|
|
@@ -183,7 +98,7 @@ export const Example = ({
|
|
|
183
98
|
// Content that appears between h3 and code block to explain example
|
|
184
99
|
children,
|
|
185
100
|
// Show dark theme switcher on full page examples
|
|
186
|
-
|
|
101
|
+
hasThemeSwitcher = process.env.hasThemeSwitcher,
|
|
187
102
|
// Show dark theme switcher on full page examples
|
|
188
103
|
hasRTLSwitcher = process.env.hasRTLSwitcher,
|
|
189
104
|
// Map of relative imports matched to their npm package import path (passed to Codesandbox)
|
|
@@ -191,7 +106,7 @@ export const Example = ({
|
|
|
191
106
|
// md file location in node_modules, used to resolve relative import paths in examples
|
|
192
107
|
relPath = '',
|
|
193
108
|
// absolute url to hosted file
|
|
194
|
-
sourceLink = ''
|
|
109
|
+
sourceLink = ''
|
|
195
110
|
}) => {
|
|
196
111
|
if (isFullscreenPreview) {
|
|
197
112
|
isFullscreen = false;
|
|
@@ -202,9 +117,7 @@ export const Example = ({
|
|
|
202
117
|
useEffect(() => {
|
|
203
118
|
if (!isFullscreenPreview) return;
|
|
204
119
|
|
|
205
|
-
document.readyState === 'complete'
|
|
206
|
-
? addPageLoadedClass()
|
|
207
|
-
: window.addEventListener('load', addPageLoadedClass);
|
|
120
|
+
document.readyState === 'complete' ? addPageLoadedClass() : window.addEventListener('load', addPageLoadedClass);
|
|
208
121
|
|
|
209
122
|
return () => window.removeEventListener('load', addPageLoadedClass);
|
|
210
123
|
}, []);
|
|
@@ -230,9 +143,7 @@ export const Example = ({
|
|
|
230
143
|
...reactCoreModule,
|
|
231
144
|
...reactTableModule,
|
|
232
145
|
...(source === 'react-next' ? reactCoreNextModule : {}),
|
|
233
|
-
...(source === 'react-deprecated'
|
|
234
|
-
? { ...reactCoreDeprecatedModule, ...reactTableDeprecatedModule }
|
|
235
|
-
: {}),
|
|
146
|
+
...(source === 'react-deprecated' ? { ...reactCoreDeprecatedModule, ...reactTableDeprecatedModule } : {})
|
|
236
147
|
};
|
|
237
148
|
|
|
238
149
|
let livePreview = null;
|
|
@@ -248,8 +159,7 @@ export const Example = ({
|
|
|
248
159
|
);
|
|
249
160
|
} else {
|
|
250
161
|
try {
|
|
251
|
-
const { code: transformedCode, hasTS } =
|
|
252
|
-
convertToReactComponent(editorCode);
|
|
162
|
+
const { code: transformedCode, hasTS } = convertToReactComponent(editorCode);
|
|
253
163
|
if (hasTS) {
|
|
254
164
|
lang = 'ts';
|
|
255
165
|
} else {
|
|
@@ -259,11 +169,7 @@ export const Example = ({
|
|
|
259
169
|
const componentNames = Object.keys(scope);
|
|
260
170
|
const componentValues = Object.values(scope);
|
|
261
171
|
|
|
262
|
-
const getPreviewComponent = new Function(
|
|
263
|
-
'React',
|
|
264
|
-
...componentNames,
|
|
265
|
-
transformedCode
|
|
266
|
-
);
|
|
172
|
+
const getPreviewComponent = new Function('React', ...componentNames, transformedCode);
|
|
267
173
|
const PreviewComponent = getPreviewComponent(React, ...componentValues);
|
|
268
174
|
|
|
269
175
|
livePreview = (
|
|
@@ -282,13 +188,13 @@ export const Example = ({
|
|
|
282
188
|
return (
|
|
283
189
|
<div id={previewId} className={css(className, 'pf-v6-u-h-100')}>
|
|
284
190
|
{livePreview}
|
|
285
|
-
{(
|
|
191
|
+
{(hasThemeSwitcher || hasRTLSwitcher) && (
|
|
286
192
|
<Flex
|
|
287
193
|
direction={{ default: 'column' }}
|
|
288
194
|
gap={{ default: 'gapMd' }}
|
|
289
195
|
className="ws-full-page-utils pf-v6-m-dir-ltr"
|
|
290
196
|
>
|
|
291
|
-
{
|
|
197
|
+
{hasThemeSwitcher && <ThemeSelector id="ws-example-theme-select" />}
|
|
292
198
|
{hasRTLSwitcher && (
|
|
293
199
|
<Switch
|
|
294
200
|
id="ws-example-rtl-switch"
|
|
@@ -310,61 +216,49 @@ export const Example = ({
|
|
|
310
216
|
const codeBoxParams = getParameters(
|
|
311
217
|
lang === 'html'
|
|
312
218
|
? getStaticParams(title, editorCode)
|
|
313
|
-
: getReactParams(
|
|
314
|
-
title,
|
|
315
|
-
editorCode,
|
|
316
|
-
scope,
|
|
317
|
-
lang,
|
|
318
|
-
relativeImports,
|
|
319
|
-
relPath,
|
|
320
|
-
sourceLink
|
|
321
|
-
)
|
|
219
|
+
: getReactParams(title, editorCode, scope, lang, relativeImports, relPath, sourceLink)
|
|
322
220
|
);
|
|
323
221
|
const fullscreenLink =
|
|
324
|
-
loc.pathname.replace(/\/$/, '') +
|
|
325
|
-
(loc.pathname.endsWith(source) ? '' : `/${source}`) +
|
|
326
|
-
'/' +
|
|
327
|
-
slugger(title);
|
|
222
|
+
loc.pathname.replace(/\/$/, '') + (loc.pathname.endsWith(source) ? '' : `/${source}`) + '/' + slugger(title);
|
|
328
223
|
|
|
329
224
|
const hasMetaText = isBeta || isDemo || isDeprecated || false;
|
|
330
|
-
const tooltips = (
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
<
|
|
334
|
-
<
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
<
|
|
343
|
-
<
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
<
|
|
352
|
-
<
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
225
|
+
const tooltips = (
|
|
226
|
+
<React.Fragment>
|
|
227
|
+
{isBeta && (
|
|
228
|
+
<Tooltip content="This beta component is currently under review and is still open for further evolution.">
|
|
229
|
+
<Button variant="plain" hasNoPadding>
|
|
230
|
+
<Label isCompact color="blue">
|
|
231
|
+
Beta
|
|
232
|
+
</Label>
|
|
233
|
+
</Button>
|
|
234
|
+
</Tooltip>
|
|
235
|
+
)}
|
|
236
|
+
{isDemo && (
|
|
237
|
+
<Tooltip content="Demos show how multiple components can be used in a single design.">
|
|
238
|
+
<Button variant="plain" hasNoPadding>
|
|
239
|
+
<Label isCompact color="purple">
|
|
240
|
+
Demo
|
|
241
|
+
</Label>
|
|
242
|
+
</Button>
|
|
243
|
+
</Tooltip>
|
|
244
|
+
)}
|
|
245
|
+
{isDeprecated && (
|
|
246
|
+
<Tooltip content="Deprecated components are available for use but are no longer being maintained or enhanced.">
|
|
247
|
+
<Button variant="plain" hasNoPadding>
|
|
248
|
+
<Label isCompact color="grey">
|
|
249
|
+
Deprecated
|
|
250
|
+
</Label>
|
|
251
|
+
</Button>
|
|
252
|
+
</Tooltip>
|
|
253
|
+
)}
|
|
254
|
+
</React.Fragment>
|
|
255
|
+
);
|
|
256
|
+
const metaText = hasMetaText && tooltips;
|
|
360
257
|
|
|
361
258
|
return (
|
|
362
259
|
<Stack hasGutter>
|
|
363
260
|
<StackItem>
|
|
364
|
-
<AutoLinkHeader
|
|
365
|
-
metaText={metaText}
|
|
366
|
-
headingLevel="h3"
|
|
367
|
-
>
|
|
261
|
+
<AutoLinkHeader metaText={metaText} headingLevel="h3">
|
|
368
262
|
{title}
|
|
369
263
|
</AutoLinkHeader>
|
|
370
264
|
{children}
|
|
@@ -378,22 +272,11 @@ export const Example = ({
|
|
|
378
272
|
target="_blank"
|
|
379
273
|
aria-label={`Open fullscreen ${title} example`}
|
|
380
274
|
>
|
|
381
|
-
<img
|
|
382
|
-
src={thumbnail.src}
|
|
383
|
-
width={thumbnail.width}
|
|
384
|
-
height={thumbnail.height}
|
|
385
|
-
alt={`${title} screenshot`}
|
|
386
|
-
/>
|
|
275
|
+
<img src={thumbnail.src} width={thumbnail.width} height={thumbnail.height} alt={`${title} screenshot`} />
|
|
387
276
|
</a>
|
|
388
277
|
</div>
|
|
389
278
|
) : (
|
|
390
|
-
<div
|
|
391
|
-
id={previewId}
|
|
392
|
-
className={css(
|
|
393
|
-
className,
|
|
394
|
-
isRTL && 'pf-v6-m-dir-rtl'
|
|
395
|
-
)}
|
|
396
|
-
>
|
|
279
|
+
<div id={previewId} className={css(className, isRTL && 'pf-v6-m-dir-rtl')}>
|
|
397
280
|
{livePreview}
|
|
398
281
|
</div>
|
|
399
282
|
)}
|
package/components/index.js
CHANGED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Select,
|
|
4
|
+
SelectGroup,
|
|
5
|
+
SelectList,
|
|
6
|
+
SelectOption,
|
|
7
|
+
MenuToggle,
|
|
8
|
+
MenuSearch,
|
|
9
|
+
MenuSearchInput,
|
|
10
|
+
ToggleGroup,
|
|
11
|
+
ToggleGroupItem,
|
|
12
|
+
Icon,
|
|
13
|
+
Divider
|
|
14
|
+
} from '@patternfly/react-core';
|
|
15
|
+
import { useTheme, THEME_TYPES } from '../../hooks/useTheme';
|
|
16
|
+
|
|
17
|
+
const SunIcon = (
|
|
18
|
+
<svg
|
|
19
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
20
|
+
viewBox="0 0 32 32"
|
|
21
|
+
className="pf-v6-svg"
|
|
22
|
+
fill="var(--pf-t--global--icon--color--regular)"
|
|
23
|
+
>
|
|
24
|
+
<path d="M16 25c-4.963 0-9-4.038-9-9s4.037-9 9-9 9 4.038 9 9-4.037 9-9 9Zm0-16c-3.86 0-7 3.14-7 7s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7Zm0-4a1 1 0 0 1-1-1V1a1 1 0 1 1 2 0v3a1 1 0 0 1-1 1Zm0 27a1 1 0 0 1-1-1v-3a1 1 0 1 1 2 0v3a1 1 0 0 1-1 1ZM4 17H1a1 1 0 1 1 0-2h3a1 1 0 1 1 0 2Zm27 0h-3a1 1 0 1 1 0-2h3a1 1 0 1 1 0 2ZM5.394 27.606a1 1 0 0 1-.707-1.707l2.12-2.12a1 1 0 1 1 1.415 1.413L6.1 27.313a.997.997 0 0 1-.707.293ZM24.485 8.515a1 1 0 0 1-.707-1.707L25.9 4.686a1 1 0 1 1 1.415 1.415l-2.122 2.12a.997.997 0 0 1-.707.294Zm-16.97 0a.997.997 0 0 1-.707-.293L4.686 6.1a1 1 0 1 1 1.415-1.415l2.12 2.122a1 1 0 0 1-.706 1.707Zm19.091 19.091a.997.997 0 0 1-.707-.293l-2.12-2.12a1 1 0 1 1 1.413-1.415l2.122 2.121a1 1 0 0 1-.707 1.707Z"></path>
|
|
25
|
+
</svg>
|
|
26
|
+
);
|
|
27
|
+
const MoonIcon = (
|
|
28
|
+
<svg
|
|
29
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
30
|
+
viewBox="0 0 32 32"
|
|
31
|
+
className="pf-v6-svg"
|
|
32
|
+
fill="var(--pf-t--global--icon--color--regular)"
|
|
33
|
+
>
|
|
34
|
+
<path d="M16.457 30C8.485 30 2 23.515 2 15.543c0-5.93 3.715-11.345 9.243-13.476a.999.999 0 0 1 1.289 1.3 12.185 12.185 0 0 0-.843 4.487c0 6.869 5.588 12.457 12.457 12.457 1.56 0 3.07-.284 4.487-.844a.998.998 0 0 1 1.3 1.29C27.802 26.285 22.387 30 16.456 30ZM9.992 4.904C6.338 7.134 4 11.177 4 15.544 4 22.412 9.588 28 16.457 28c4.367 0 8.41-2.338 10.639-5.992a14.39 14.39 0 0 1-2.95.302c-7.971 0-14.457-6.485-14.457-14.456 0-1.003.102-1.989.303-2.95Z"></path>
|
|
35
|
+
</svg>
|
|
36
|
+
);
|
|
37
|
+
const DesktopIcon = (
|
|
38
|
+
<svg
|
|
39
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
40
|
+
viewBox="0 0 32 32"
|
|
41
|
+
className="pf-v6-svg"
|
|
42
|
+
fill="var(--pf-t--global--icon--color--regular)"
|
|
43
|
+
>
|
|
44
|
+
<path d="M23.94 16a1 1 0 0 1-.992-.876 6.957 6.957 0 0 0-6.069-6.062 1 1 0 1 1 .242-1.985 8.953 8.953 0 0 1 7.812 7.8A.999.999 0 0 1 23.94 16ZM16 5a1 1 0 0 1-1-1V1a1 1 0 1 1 2 0v3a1 1 0 0 1-1 1Zm0 27a1 1 0 0 1-1-1v-3a1 1 0 1 1 2 0v3a1 1 0 0 1-1 1ZM4 17H1a1 1 0 1 1 0-2h3a1 1 0 1 1 0 2Zm27 0h-3a1 1 0 1 1 0-2h3a1 1 0 1 1 0 2ZM5.394 27.606a1 1 0 0 1-.707-1.707l2.12-2.12a1 1 0 1 1 1.415 1.413L6.1 27.313a.997.997 0 0 1-.707.293ZM24.485 8.515a1 1 0 0 1-.707-1.707L25.9 4.686a1 1 0 1 1 1.415 1.415l-2.122 2.12a.997.997 0 0 1-.707.294Zm-16.97 0a.997.997 0 0 1-.707-.293L4.686 6.1a1 1 0 1 1 1.415-1.415l2.12 2.122a1 1 0 0 1-.706 1.707Zm19.091 19.091a.997.997 0 0 1-.707-.293l-2.12-2.12a1 1 0 1 1 1.413-1.415l2.122 2.121a1 1 0 0 1-.707 1.707ZM16 24.875c-4.894 0-8.875-3.981-8.875-8.875a8.879 8.879 0 0 1 5.227-8.088.876.876 0 0 1 1.153 1.163 6.945 6.945 0 0 0-.63 2.925A7.133 7.133 0 0 0 20 19.125a6.948 6.948 0 0 0 2.925-.63.876.876 0 0 1 1.163 1.154A8.88 8.88 0 0 1 16 24.875Zm-4.785-14.153A7.135 7.135 0 0 0 8.875 16 7.133 7.133 0 0 0 16 23.125a7.13 7.13 0 0 0 5.278-2.34c-.419.06-.845.09-1.278.09-4.894 0-8.875-3.981-8.875-8.875 0-.433.03-.86.09-1.278Z"></path>
|
|
45
|
+
</svg>
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
export const ThemeSelector = ({ id }) => {
|
|
49
|
+
const { mode: themeMode, setMode: setThemeMode, modes: colorModes } = useTheme(THEME_TYPES.COLOR);
|
|
50
|
+
const {
|
|
51
|
+
mode: highContrastMode,
|
|
52
|
+
setMode: setHighContrastMode,
|
|
53
|
+
modes: highContrastModes
|
|
54
|
+
} = useTheme(THEME_TYPES.HIGH_CONTRAST);
|
|
55
|
+
const [isThemeSelectOpen, setIsThemeSelectOpen] = useState(false);
|
|
56
|
+
|
|
57
|
+
const handleThemeChange = (_event, selectedMode) => {
|
|
58
|
+
setThemeMode(selectedMode);
|
|
59
|
+
setIsThemeSelectOpen(false);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const handleHighContrastThemeSelection = (evt) => {
|
|
63
|
+
setHighContrastMode(evt.currentTarget.id);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const getThemeDisplayText = (mode) => {
|
|
67
|
+
switch (mode) {
|
|
68
|
+
case colorModes.LIGHT:
|
|
69
|
+
return 'Light';
|
|
70
|
+
case colorModes.DARK:
|
|
71
|
+
return 'Dark';
|
|
72
|
+
default:
|
|
73
|
+
return 'System';
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const getThemeIcon = (mode) => {
|
|
78
|
+
switch (mode) {
|
|
79
|
+
case colorModes.LIGHT:
|
|
80
|
+
return SunIcon;
|
|
81
|
+
case colorModes.DARK:
|
|
82
|
+
return MoonIcon;
|
|
83
|
+
default:
|
|
84
|
+
return DesktopIcon;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
return (
|
|
88
|
+
<Select
|
|
89
|
+
id={id}
|
|
90
|
+
isOpen={isThemeSelectOpen}
|
|
91
|
+
selected={themeMode}
|
|
92
|
+
onSelect={handleThemeChange}
|
|
93
|
+
onOpenChange={(isOpen) => setIsThemeSelectOpen(isOpen)}
|
|
94
|
+
toggle={(toggleRef) => (
|
|
95
|
+
<MenuToggle
|
|
96
|
+
ref={toggleRef}
|
|
97
|
+
onClick={() => setIsThemeSelectOpen(!isThemeSelectOpen)}
|
|
98
|
+
isExpanded={isThemeSelectOpen}
|
|
99
|
+
icon={<Icon size="lg">{getThemeIcon(themeMode)}</Icon>}
|
|
100
|
+
aria-label={`Theme selection, current: ${getThemeDisplayText(themeMode)}`}
|
|
101
|
+
/>
|
|
102
|
+
)}
|
|
103
|
+
shouldFocusToggleOnSelect
|
|
104
|
+
onOpenChangeKeys={['Escape']}
|
|
105
|
+
>
|
|
106
|
+
<SelectGroup>
|
|
107
|
+
<SelectList aria-label="Light/Dark theme switcher">
|
|
108
|
+
<SelectOption value={colorModes.SYSTEM} icon={DesktopIcon} description="Follow system preference">
|
|
109
|
+
System
|
|
110
|
+
</SelectOption>
|
|
111
|
+
<SelectOption value={colorModes.LIGHT} icon={SunIcon} description="Always use light mode">
|
|
112
|
+
Light
|
|
113
|
+
</SelectOption>
|
|
114
|
+
<SelectOption value={colorModes.DARK} icon={MoonIcon} description="Always use dark mode">
|
|
115
|
+
Dark
|
|
116
|
+
</SelectOption>
|
|
117
|
+
</SelectList>
|
|
118
|
+
</SelectGroup>
|
|
119
|
+
<Divider />
|
|
120
|
+
<SelectGroup label="High Contrast">
|
|
121
|
+
<MenuSearch>
|
|
122
|
+
<MenuSearchInput>
|
|
123
|
+
<ToggleGroup aria-label="High contrast theme switcher">
|
|
124
|
+
<ToggleGroupItem
|
|
125
|
+
text="System"
|
|
126
|
+
buttonId={highContrastModes.SYSTEM}
|
|
127
|
+
isSelected={highContrastMode === highContrastModes.SYSTEM}
|
|
128
|
+
onChange={handleHighContrastThemeSelection}
|
|
129
|
+
/>
|
|
130
|
+
<ToggleGroupItem
|
|
131
|
+
text="On"
|
|
132
|
+
buttonId={highContrastModes.ON}
|
|
133
|
+
isSelected={highContrastMode === highContrastModes.ON}
|
|
134
|
+
onChange={handleHighContrastThemeSelection}
|
|
135
|
+
/>
|
|
136
|
+
<ToggleGroupItem
|
|
137
|
+
text="Off"
|
|
138
|
+
buttonId={highContrastModes.OFF}
|
|
139
|
+
isSelected={highContrastMode === highContrastModes.OFF}
|
|
140
|
+
onChange={handleHighContrastThemeSelection}
|
|
141
|
+
/>
|
|
142
|
+
</ToggleGroup>
|
|
143
|
+
</MenuSearchInput>
|
|
144
|
+
</MenuSearch>
|
|
145
|
+
</SelectGroup>
|
|
146
|
+
</Select>
|
|
147
|
+
);
|
|
148
|
+
};
|
package/hooks/useTheme.js
CHANGED
|
@@ -1,137 +1,174 @@
|
|
|
1
|
-
import { useState, useEffect
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const COLOR_MODES = {
|
|
4
4
|
SYSTEM: 'system',
|
|
5
5
|
LIGHT: 'light',
|
|
6
6
|
DARK: 'dark'
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
-
const
|
|
10
|
-
|
|
9
|
+
const HIGH_CONTRAST_MODES = {
|
|
10
|
+
SYSTEM: 'high-contrast-system',
|
|
11
|
+
ON: 'high-contrast-on',
|
|
12
|
+
OFF: 'high-contrast-off'
|
|
13
|
+
};
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
15
|
+
export const THEME_TYPES = {
|
|
16
|
+
COLOR: 'color',
|
|
17
|
+
HIGH_CONTRAST: 'high-contrast'
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
class ThemeManager {
|
|
21
|
+
constructor({ storageKey, modes, defaultMode, cssClass, classEnabledMode, mediaQueryString }) {
|
|
22
|
+
this.storageKey = storageKey;
|
|
23
|
+
this.modes = modes;
|
|
24
|
+
this.defaultMode = defaultMode;
|
|
25
|
+
this.cssClass = cssClass;
|
|
26
|
+
this.classEnabledMode = classEnabledMode;
|
|
27
|
+
this.mediaQueryString = mediaQueryString;
|
|
28
|
+
this.isBrowser = typeof window !== 'undefined' && window.matchMedia && window.localStorage;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
getMediaQuery() {
|
|
32
|
+
if (!this.isBrowser) {
|
|
33
|
+
return;
|
|
24
34
|
}
|
|
25
|
-
return
|
|
26
|
-
}
|
|
35
|
+
return window.matchMedia(this.mediaQueryString);
|
|
36
|
+
}
|
|
27
37
|
|
|
28
|
-
|
|
29
|
-
if (
|
|
38
|
+
getStoredValue() {
|
|
39
|
+
if (!this.isBrowser) {
|
|
30
40
|
return;
|
|
31
41
|
}
|
|
32
|
-
localStorage.
|
|
33
|
-
}
|
|
42
|
+
return localStorage.getItem(this.storageKey);
|
|
43
|
+
}
|
|
34
44
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return 'light';
|
|
45
|
+
setStoredValue(value) {
|
|
46
|
+
if (!this.isBrowser) {
|
|
47
|
+
return;
|
|
39
48
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
console.warn('matchMedia not supported, defaulting to light theme');
|
|
47
|
-
return 'light';
|
|
48
|
-
}
|
|
49
|
+
localStorage.setItem(this.storageKey, value);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
resolve() {
|
|
53
|
+
if (!this.isBrowser) {
|
|
54
|
+
return this.defaultMode;
|
|
49
55
|
}
|
|
50
|
-
return mode;
|
|
51
|
-
};
|
|
52
56
|
|
|
53
|
-
|
|
54
|
-
|
|
57
|
+
if (window.matchMedia(this.mediaQueryString).matches) {
|
|
58
|
+
return this.classEnabledMode;
|
|
59
|
+
}
|
|
60
|
+
return this.defaultMode;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
addClass() {
|
|
64
|
+
if (!this.isBrowser) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
document.querySelector('html').classList.add(this.cssClass);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
removeClass() {
|
|
71
|
+
if (!this.isBrowser) {
|
|
55
72
|
return;
|
|
56
73
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
74
|
+
document.querySelector('html').classList.remove(this.cssClass);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
updateClass(mode) {
|
|
78
|
+
if (!this.isBrowser) {
|
|
60
79
|
return;
|
|
61
80
|
}
|
|
62
|
-
|
|
63
|
-
if (
|
|
64
|
-
|
|
81
|
+
|
|
82
|
+
if (mode === this.modes.SYSTEM) {
|
|
83
|
+
if (this.resolve() === this.classEnabledMode) {
|
|
84
|
+
this.addClass();
|
|
85
|
+
} else {
|
|
86
|
+
this.removeClass();
|
|
87
|
+
}
|
|
65
88
|
} else {
|
|
66
|
-
|
|
89
|
+
if (mode === this.classEnabledMode) {
|
|
90
|
+
this.addClass();
|
|
91
|
+
} else {
|
|
92
|
+
this.removeClass();
|
|
93
|
+
}
|
|
67
94
|
}
|
|
68
|
-
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const themeRegistry = new Map();
|
|
99
|
+
|
|
100
|
+
const registerThemeManager = (themeType, manager) => {
|
|
101
|
+
themeRegistry.set(themeType, manager);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const colorThemeManager = new ThemeManager({
|
|
105
|
+
storageKey: 'theme-preference',
|
|
106
|
+
modes: COLOR_MODES,
|
|
107
|
+
defaultMode: COLOR_MODES.SYSTEM,
|
|
108
|
+
cssClass: 'pf-v6-theme-dark',
|
|
109
|
+
classEnabledMode: COLOR_MODES.DARK,
|
|
110
|
+
mediaQueryString: '(prefers-color-scheme: dark)'
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const highContrastThemeManager = new ThemeManager({
|
|
114
|
+
storageKey: 'high-contrast-preference',
|
|
115
|
+
modes: HIGH_CONTRAST_MODES,
|
|
116
|
+
defaultMode: HIGH_CONTRAST_MODES.SYSTEM,
|
|
117
|
+
cssClass: 'pf-v6-theme-high-contrast',
|
|
118
|
+
classEnabledMode: HIGH_CONTRAST_MODES.ON,
|
|
119
|
+
mediaQueryString: '(prefers-contrast: more)'
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
registerThemeManager(THEME_TYPES.COLOR, colorThemeManager);
|
|
123
|
+
registerThemeManager(THEME_TYPES.HIGH_CONTRAST, highContrastThemeManager);
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Unified theme hook that accepts a theme type parameter
|
|
127
|
+
* @param {string} themeType - The type of theme to manage (THEME_TYPES.COLOR, THEME_TYPES.HIGH_CONTRAST, instantiate and register new themes above as needed)
|
|
128
|
+
* @returns {Object} Theme state and controls specific to the theme type
|
|
129
|
+
*/
|
|
130
|
+
export const useTheme = (themeType) => {
|
|
131
|
+
if (!themeType) {
|
|
132
|
+
throw new Error('useTheme requires a theme type parameter. Use THEME_TYPES.COLOR or THEME_TYPES.HIGH_CONTRAST');
|
|
133
|
+
}
|
|
69
134
|
|
|
70
|
-
const
|
|
71
|
-
const stored = getStoredThemeMode();
|
|
72
|
-
return stored && Object.values(THEME_MODES).includes(stored) ? stored : THEME_MODES.SYSTEM;
|
|
73
|
-
});
|
|
135
|
+
const theme = themeRegistry.get(themeType);
|
|
74
136
|
|
|
75
|
-
|
|
137
|
+
if (!theme) {
|
|
138
|
+
throw new Error(`Theme manager not found for theme type: ${themeType}`);
|
|
139
|
+
}
|
|
76
140
|
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
setStoredThemeMode(newMode);
|
|
80
|
-
|
|
81
|
-
const newResolvedTheme = getResolvedTheme(newMode);
|
|
82
|
-
setResolvedTheme(newResolvedTheme);
|
|
83
|
-
updateThemeClass(newResolvedTheme);
|
|
84
|
-
}, []);
|
|
141
|
+
const [mode, setMode] = useState(theme.getStoredValue());
|
|
142
|
+
const [resolvedTheme, setResolvedTheme] = useState(theme.resolve());
|
|
85
143
|
|
|
86
|
-
// Listen for system preference changes
|
|
87
144
|
useEffect(() => {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
145
|
+
theme.setStoredValue(mode);
|
|
146
|
+
theme.updateClass(mode);
|
|
147
|
+
}, [theme, mode, resolvedTheme]);
|
|
92
148
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
console.warn('matchMedia not supported, skipping system theme detection');
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const handleSystemThemeChange = (e) => {
|
|
102
|
-
if (themeMode === THEME_MODES.SYSTEM) {
|
|
103
|
-
const newSystemTheme = e.matches ? 'dark' : 'light';
|
|
104
|
-
setResolvedTheme(newSystemTheme);
|
|
105
|
-
updateThemeClass(newSystemTheme);
|
|
106
|
-
}
|
|
107
|
-
};
|
|
149
|
+
const handlePreferenceChange = () => {
|
|
150
|
+
setResolvedTheme(theme.resolve());
|
|
151
|
+
};
|
|
152
|
+
const mediaQuery = theme.getMediaQuery();
|
|
108
153
|
|
|
109
|
-
|
|
154
|
+
useEffect(() => {
|
|
110
155
|
if (mediaQuery.addEventListener) {
|
|
111
|
-
mediaQuery.addEventListener('change',
|
|
156
|
+
mediaQuery.addEventListener('change', handlePreferenceChange);
|
|
112
157
|
return () => {
|
|
113
|
-
mediaQuery.removeEventListener('change',
|
|
158
|
+
mediaQuery.removeEventListener('change', handlePreferenceChange);
|
|
114
159
|
};
|
|
115
160
|
} else if (mediaQuery.addListener) {
|
|
116
|
-
|
|
117
|
-
mediaQuery.addListener(handleSystemThemeChange);
|
|
161
|
+
mediaQuery.addListener(handlePreferenceChange);
|
|
118
162
|
return () => {
|
|
119
|
-
mediaQuery.removeListener(
|
|
163
|
+
mediaQuery.removeListener(handlePreferenceChange);
|
|
120
164
|
};
|
|
121
165
|
}
|
|
122
|
-
}, [
|
|
123
|
-
|
|
124
|
-
// Initial theme application
|
|
125
|
-
useEffect(() => {
|
|
126
|
-
const initialResolvedTheme = getResolvedTheme(themeMode);
|
|
127
|
-
setResolvedTheme(initialResolvedTheme);
|
|
128
|
-
updateThemeClass(initialResolvedTheme);
|
|
129
|
-
}, [themeMode]);
|
|
166
|
+
}, [mediaQuery]);
|
|
130
167
|
|
|
131
168
|
return {
|
|
132
|
-
|
|
133
|
-
|
|
169
|
+
mode,
|
|
170
|
+
setMode,
|
|
134
171
|
resolvedTheme,
|
|
135
|
-
|
|
172
|
+
modes: theme.modes
|
|
136
173
|
};
|
|
137
|
-
};
|
|
174
|
+
};
|
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
DropdownGroup,
|
|
10
10
|
DropdownList,
|
|
11
11
|
Divider,
|
|
12
|
-
Icon,
|
|
13
12
|
Masthead,
|
|
14
13
|
MastheadToggle,
|
|
15
14
|
MastheadMain,
|
|
@@ -24,20 +23,14 @@ import {
|
|
|
24
23
|
SkipToContent,
|
|
25
24
|
Switch,
|
|
26
25
|
SearchInput,
|
|
27
|
-
Select,
|
|
28
|
-
SelectOption,
|
|
29
|
-
SelectList,
|
|
30
26
|
MastheadLogo
|
|
31
27
|
} from '@patternfly/react-core';
|
|
32
28
|
import BarsIcon from '@patternfly/react-icons/dist/esm/icons/bars-icon';
|
|
33
29
|
import GithubIcon from '@patternfly/react-icons/dist/esm/icons/github-icon';
|
|
34
|
-
|
|
35
|
-
const MoonIcon = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" className="pf-v6-svg" fill="var(--pf-t--global--icon--color--regular)"><path d="M16.457 30C8.485 30 2 23.515 2 15.543c0-5.93 3.715-11.345 9.243-13.476a.999.999 0 0 1 1.289 1.3 12.185 12.185 0 0 0-.843 4.487c0 6.869 5.588 12.457 12.457 12.457 1.56 0 3.07-.284 4.487-.844a.998.998 0 0 1 1.3 1.29C27.802 26.285 22.387 30 16.456 30ZM9.992 4.904C6.338 7.134 4 11.177 4 15.544 4 22.412 9.588 28 16.457 28c4.367 0 8.41-2.338 10.639-5.992a14.39 14.39 0 0 1-2.95.302c-7.971 0-14.457-6.485-14.457-14.456 0-1.003.102-1.989.303-2.95Z"></path></svg>;
|
|
36
|
-
const DesktopIcon = <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" className="pf-v6-svg" fill="var(--pf-t--global--icon--color--regular)"><path d="M23.94 16a1 1 0 0 1-.992-.876 6.957 6.957 0 0 0-6.069-6.062 1 1 0 1 1 .242-1.985 8.953 8.953 0 0 1 7.812 7.8A.999.999 0 0 1 23.94 16ZM16 5a1 1 0 0 1-1-1V1a1 1 0 1 1 2 0v3a1 1 0 0 1-1 1Zm0 27a1 1 0 0 1-1-1v-3a1 1 0 1 1 2 0v3a1 1 0 0 1-1 1ZM4 17H1a1 1 0 1 1 0-2h3a1 1 0 1 1 0 2Zm27 0h-3a1 1 0 1 1 0-2h3a1 1 0 1 1 0 2ZM5.394 27.606a1 1 0 0 1-.707-1.707l2.12-2.12a1 1 0 1 1 1.415 1.413L6.1 27.313a.997.997 0 0 1-.707.293ZM24.485 8.515a1 1 0 0 1-.707-1.707L25.9 4.686a1 1 0 1 1 1.415 1.415l-2.122 2.12a.997.997 0 0 1-.707.294Zm-16.97 0a.997.997 0 0 1-.707-.293L4.686 6.1a1 1 0 1 1 1.415-1.415l2.12 2.122a1 1 0 0 1-.706 1.707Zm19.091 19.091a.997.997 0 0 1-.707-.293l-2.12-2.12a1 1 0 1 1 1.413-1.415l2.122 2.121a1 1 0 0 1-.707 1.707ZM16 24.875c-4.894 0-8.875-3.981-8.875-8.875a8.879 8.879 0 0 1 5.227-8.088.876.876 0 0 1 1.153 1.163 6.945 6.945 0 0 0-.63 2.925A7.133 7.133 0 0 0 20 19.125a6.948 6.948 0 0 0 2.925-.63.876.876 0 0 1 1.163 1.154A8.88 8.88 0 0 1 16 24.875Zm-4.785-14.153A7.135 7.135 0 0 0 8.875 16 7.133 7.133 0 0 0 16 23.125a7.13 7.13 0 0 0 5.278-2.34c-.419.06-.845.09-1.278.09-4.894 0-8.875-3.981-8.875-8.875 0-.433.03-.86.09-1.278Z"></path></svg>;
|
|
37
|
-
import { SideNav, TopNav, GdprBanner } from '../../components';
|
|
30
|
+
import { SideNav, TopNav, GdprBanner, ThemeSelector } from '../../components';
|
|
38
31
|
import staticVersions from '../../versions.json';
|
|
39
32
|
import { Footer } from '@patternfly/documentation-framework/components';
|
|
40
|
-
import { useTheme } from '../../hooks/useTheme';
|
|
33
|
+
import { useTheme, THEME_TYPES } from '../../hooks/useTheme';
|
|
41
34
|
|
|
42
35
|
export const RtlContext = createContext(false);
|
|
43
36
|
|
|
@@ -45,14 +38,11 @@ const HeaderTools = ({
|
|
|
45
38
|
versions,
|
|
46
39
|
hasVersionSwitcher,
|
|
47
40
|
algolia,
|
|
48
|
-
|
|
41
|
+
hasThemeSwitcher,
|
|
49
42
|
hasRTLSwitcher,
|
|
50
43
|
topNavItems,
|
|
51
44
|
isRTL,
|
|
52
45
|
setIsRTL,
|
|
53
|
-
themeMode,
|
|
54
|
-
setThemeMode,
|
|
55
|
-
THEME_MODES
|
|
56
46
|
}) => {
|
|
57
47
|
const latestVersion = versions.Releases.find((version) => version.latest);
|
|
58
48
|
const previousReleases = Object.values(versions.Releases).filter((version) => !version.hidden && !version.latest);
|
|
@@ -60,7 +50,6 @@ const HeaderTools = ({
|
|
|
60
50
|
const [isDropdownOpen, setDropdownOpen] = useState(false);
|
|
61
51
|
const [searchValue, setSearchValue] = React.useState('');
|
|
62
52
|
const [isSearchExpanded, setIsSearchExpanded] = React.useState(false);
|
|
63
|
-
const [isThemeSelectOpen, setIsThemeSelectOpen] = useState(false);
|
|
64
53
|
|
|
65
54
|
const getDropdownItem = (version, isLatest = false) => (
|
|
66
55
|
<DropdownItem itemId={version.name} key={version.name} to={isLatest ? '/' : `/${version.name}`}>
|
|
@@ -76,33 +65,6 @@ const HeaderTools = ({
|
|
|
76
65
|
setIsSearchExpanded(!isSearchExpanded);
|
|
77
66
|
};
|
|
78
67
|
|
|
79
|
-
const handleThemeChange = (_event, selectedMode) => {
|
|
80
|
-
setThemeMode(selectedMode);
|
|
81
|
-
setIsThemeSelectOpen(false);
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
const getThemeDisplayText = (mode) => {
|
|
85
|
-
switch (mode) {
|
|
86
|
-
case THEME_MODES.LIGHT:
|
|
87
|
-
return 'Light';
|
|
88
|
-
case THEME_MODES.DARK:
|
|
89
|
-
return 'Dark';
|
|
90
|
-
default:
|
|
91
|
-
return 'System';
|
|
92
|
-
}
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
const getThemeIcon = (mode) => {
|
|
96
|
-
switch (mode) {
|
|
97
|
-
case THEME_MODES.LIGHT:
|
|
98
|
-
return SunIcon;
|
|
99
|
-
case THEME_MODES.DARK:
|
|
100
|
-
return MoonIcon;
|
|
101
|
-
default:
|
|
102
|
-
return DesktopIcon;
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
|
|
106
68
|
useEffect(() => {
|
|
107
69
|
// reattach algolia to input each time search is expanded
|
|
108
70
|
if (hasSearch && isSearchExpanded) {
|
|
@@ -156,49 +118,9 @@ const HeaderTools = ({
|
|
|
156
118
|
<GithubIcon />
|
|
157
119
|
</Button>
|
|
158
120
|
</ToolbarItem>
|
|
159
|
-
{
|
|
121
|
+
{hasThemeSwitcher && (
|
|
160
122
|
<ToolbarItem>
|
|
161
|
-
<
|
|
162
|
-
id="ws-theme-select"
|
|
163
|
-
isOpen={isThemeSelectOpen}
|
|
164
|
-
selected={themeMode}
|
|
165
|
-
onSelect={handleThemeChange}
|
|
166
|
-
onOpenChange={(isOpen) => setIsThemeSelectOpen(isOpen)}
|
|
167
|
-
toggle={(toggleRef) => (
|
|
168
|
-
<MenuToggle
|
|
169
|
-
ref={toggleRef}
|
|
170
|
-
onClick={() => setIsThemeSelectOpen(!isThemeSelectOpen)}
|
|
171
|
-
isExpanded={isThemeSelectOpen}
|
|
172
|
-
icon={<Icon size="lg">{getThemeIcon(themeMode)}</Icon>}
|
|
173
|
-
aria-label={`Theme selection, current: ${getThemeDisplayText(themeMode)}`}
|
|
174
|
-
/>
|
|
175
|
-
)}
|
|
176
|
-
shouldFocusToggleOnSelect
|
|
177
|
-
>
|
|
178
|
-
<SelectList>
|
|
179
|
-
<SelectOption
|
|
180
|
-
value={THEME_MODES.SYSTEM}
|
|
181
|
-
icon={DesktopIcon}
|
|
182
|
-
description="Follow system preference"
|
|
183
|
-
>
|
|
184
|
-
System
|
|
185
|
-
</SelectOption>
|
|
186
|
-
<SelectOption
|
|
187
|
-
value={THEME_MODES.LIGHT}
|
|
188
|
-
icon={SunIcon}
|
|
189
|
-
description="Always use light mode"
|
|
190
|
-
>
|
|
191
|
-
Light
|
|
192
|
-
</SelectOption>
|
|
193
|
-
<SelectOption
|
|
194
|
-
value={THEME_MODES.DARK}
|
|
195
|
-
icon={MoonIcon}
|
|
196
|
-
description="Always use dark mode"
|
|
197
|
-
>
|
|
198
|
-
Dark
|
|
199
|
-
</SelectOption>
|
|
200
|
-
</SelectList>
|
|
201
|
-
</Select>
|
|
123
|
+
<ThemeSelector id="ws-theme-select" />
|
|
202
124
|
</ToolbarItem>
|
|
203
125
|
)}
|
|
204
126
|
{hasVersionSwitcher && (
|
|
@@ -290,7 +212,7 @@ export const SideNavLayout = ({ children, groupedRoutes, navOpen: navOpenProp })
|
|
|
290
212
|
const algolia = process.env.algolia;
|
|
291
213
|
const hasGdprBanner = process.env.hasGdprBanner;
|
|
292
214
|
const hasVersionSwitcher = process.env.hasVersionSwitcher;
|
|
293
|
-
const
|
|
215
|
+
const hasThemeSwitcher = process.env.hasThemeSwitcher;
|
|
294
216
|
const hasRTLSwitcher = process.env.hasRTLSwitcher;
|
|
295
217
|
const sideNavItems = process.env.sideNavItems;
|
|
296
218
|
const topNavItems = process.env.topNavItems;
|
|
@@ -299,8 +221,8 @@ export const SideNavLayout = ({ children, groupedRoutes, navOpen: navOpenProp })
|
|
|
299
221
|
|
|
300
222
|
const [versions, setVersions] = useState({ ...staticVersions });
|
|
301
223
|
const [isRTL, setIsRTL] = useState(false);
|
|
302
|
-
|
|
303
|
-
const {
|
|
224
|
+
|
|
225
|
+
const { resolvedTheme } = useTheme(THEME_TYPES.COLOR);
|
|
304
226
|
|
|
305
227
|
useEffect(() => {
|
|
306
228
|
if (typeof window === 'undefined') {
|
|
@@ -388,14 +310,11 @@ export const SideNavLayout = ({ children, groupedRoutes, navOpen: navOpenProp })
|
|
|
388
310
|
versions={versions}
|
|
389
311
|
algolia={algolia}
|
|
390
312
|
hasVersionSwitcher={hasVersionSwitcher}
|
|
391
|
-
|
|
313
|
+
hasThemeSwitcher={hasThemeSwitcher}
|
|
392
314
|
hasRTLSwitcher={hasRTLSwitcher}
|
|
393
315
|
topNavItems={topNavItems}
|
|
394
316
|
isRTL={isRTL}
|
|
395
317
|
setIsRTL={setIsRTL}
|
|
396
|
-
themeMode={themeMode}
|
|
397
|
-
setThemeMode={setThemeMode}
|
|
398
|
-
THEME_MODES={THEME_MODES}
|
|
399
318
|
/>
|
|
400
319
|
)}
|
|
401
320
|
</MastheadContent>
|
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.
|
|
4
|
+
"version": "6.19.0",
|
|
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.
|
|
15
|
+
"@patternfly/ast-helpers": "^1.4.0-alpha.266",
|
|
16
16
|
"@reach/router": "npm:@gatsbyjs/reach-router@1.3.9",
|
|
17
17
|
"autoprefixer": "10.4.19",
|
|
18
18
|
"babel-loader": "^9.1.3",
|
|
@@ -87,5 +87,5 @@
|
|
|
87
87
|
"braces": ">=3.0.3",
|
|
88
88
|
"ssri": ">=8.0.1"
|
|
89
89
|
},
|
|
90
|
-
"gitHead": "
|
|
90
|
+
"gitHead": "60954ecaf27cf8c2449a5284f3991bc3059870d7"
|
|
91
91
|
}
|
package/scripts/md/typecheck.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
|
-
const {
|
|
2
|
+
const { sync } = require('glob');
|
|
3
3
|
const ts = require('typescript');
|
|
4
4
|
const versions = require('../../versions.json');
|
|
5
5
|
|
|
@@ -32,7 +32,7 @@ declare module '\\*.svg' {
|
|
|
32
32
|
`;
|
|
33
33
|
|
|
34
34
|
const reactStylesDir = path.join(require.resolve('@patternfly/react-styles/package.json'), '../');
|
|
35
|
-
const reactStyles =
|
|
35
|
+
const reactStyles = sync(path.join(reactStylesDir, 'css/**/*.d.ts'))
|
|
36
36
|
.map(f => f.replace(reactStylesDir, '@patternfly/react-styles/').replace(/\.d.ts$/, ''));
|
|
37
37
|
const defaultImports = [
|
|
38
38
|
'react',
|
|
@@ -12,7 +12,7 @@ module.exports = (_env, argv) => {
|
|
|
12
12
|
hasFooter = false,
|
|
13
13
|
hasVersionSwitcher = false,
|
|
14
14
|
hasDesignGuidelines = false,
|
|
15
|
-
|
|
15
|
+
hasThemeSwitcher = false,
|
|
16
16
|
hasRTLSwitcher = false,
|
|
17
17
|
componentsData = {},
|
|
18
18
|
sideNavItems = [],
|
|
@@ -141,7 +141,7 @@ module.exports = (_env, argv) => {
|
|
|
141
141
|
'process.env.hasFooter': JSON.stringify(hasFooter),
|
|
142
142
|
'process.env.hasVersionSwitcher': JSON.stringify(hasVersionSwitcher),
|
|
143
143
|
'process.env.hasDesignGuidelines': JSON.stringify(hasDesignGuidelines),
|
|
144
|
-
'process.env.
|
|
144
|
+
'process.env.hasThemeSwitcher': JSON.stringify(hasThemeSwitcher),
|
|
145
145
|
'process.env.hasRTLSwitcher': JSON.stringify(hasRTLSwitcher),
|
|
146
146
|
'process.env.componentsData': JSON.stringify(componentsData),
|
|
147
147
|
'process.env.sideNavItems': JSON.stringify(sideNavItems),
|