left border color to the semantic token for non-status (purple) */ 10 | --pf-v6-c-content--hr--BackgroundColor: var(--pf-t--global--color--nonstatus--yellow--default); /* changes a
color to the semantic token for non-status (yellow) */ 11 | } 12 | -------------------------------------------------------------------------------- /src/app/app.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import App from '@app/index'; 3 | import { render, screen } from '@testing-library/react'; 4 | import userEvent from '@testing-library/user-event'; 5 | import '@testing-library/jest-dom'; 6 | 7 | describe('App tests', () => { 8 | test('should render default App component', () => { 9 | const { asFragment } = render(); 10 | 11 | expect(asFragment()).toMatchSnapshot(); 12 | }); 13 | 14 | it('should render a nav-toggle button', () => { 15 | render( ); 16 | 17 | expect(screen.getByRole('button', { name: 'Global navigation' })).toBeVisible(); 18 | }); 19 | 20 | // I'm fairly sure that this test not going to work properly no matter what we do since JSDOM doesn't actually 21 | // draw anything. We could potentially make something work, likely using a different test environment, but 22 | // using Cypress for this kind of test would be more efficient. 23 | it.skip('should hide the sidebar on smaller viewports', () => { 24 | Object.defineProperty(window, 'innerWidth', { writable: true, configurable: true, value: 600 }); 25 | 26 | render( ); 27 | 28 | window.dispatchEvent(new Event('resize')); 29 | 30 | expect(screen.queryByRole('link', { name: 'Dashboard' })).not.toBeInTheDocument(); 31 | }); 32 | 33 | it('should expand the sidebar on larger viewports', () => { 34 | render( ); 35 | 36 | window.dispatchEvent(new Event('resize')); 37 | 38 | expect(screen.getByRole('link', { name: 'Dashboard' })).toBeVisible(); 39 | }); 40 | 41 | it('should hide the sidebar when clicking the nav-toggle button', async () => { 42 | const user = userEvent.setup(); 43 | 44 | render( ); 45 | 46 | window.dispatchEvent(new Event('resize')); 47 | const button = screen.getByRole('button', { name: 'Global navigation' }); 48 | 49 | expect(screen.getByRole('link', { name: 'Dashboard' })).toBeVisible(); 50 | 51 | await user.click(button); 52 | 53 | expect(screen.queryByRole('link', { name: 'Dashboard' })).not.toBeInTheDocument(); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/app/bgimages/Patternfly-Logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import '@patternfly/react-core/dist/styles/base.css'; 3 | import { BrowserRouter as Router } from 'react-router-dom'; 4 | import { AppLayout } from '@app/AppLayout/AppLayout'; 5 | import { AppRoutes } from '@app/routes'; 6 | import '@app/app.css'; 7 | 8 | const App: React.FunctionComponent = () => ( 9 | 10 | 14 | ); 15 | 16 | export default App; 17 | -------------------------------------------------------------------------------- /src/app/routes.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Route, Routes } from 'react-router-dom'; 3 | import { Dashboard } from '@app/Dashboard/Dashboard'; 4 | import { Support } from '@app/Support/Support'; 5 | import { GeneralSettings } from '@app/Settings/General/GeneralSettings'; 6 | import { ProfileSettings } from '@app/Settings/Profile/ProfileSettings'; 7 | import { NotFound } from '@app/NotFound/NotFound'; 8 | 9 | export interface IAppRoute { 10 | label?: string; // Excluding the label will exclude the route from the nav sidebar in AppLayout 11 | /* eslint-disable @typescript-eslint/no-explicit-any */ 12 | element: React.ReactElement; 13 | /* eslint-enable @typescript-eslint/no-explicit-any */ 14 | exact?: boolean; 15 | path: string; 16 | title: string; 17 | routes?: undefined; 18 | } 19 | 20 | export interface IAppRouteGroup { 21 | label: string; 22 | routes: IAppRoute[]; 23 | } 24 | 25 | export type AppRouteConfig = IAppRoute | IAppRouteGroup; 26 | 27 | const routes: AppRouteConfig[] = [ 28 | { 29 | element:11 | 13 |12 | , 30 | exact: true, 31 | label: 'Dashboard', 32 | path: '/', 33 | title: 'PatternFly Seed | Main Dashboard', 34 | }, 35 | { 36 | element: , 37 | exact: true, 38 | label: 'Support', 39 | path: '/support', 40 | title: 'PatternFly Seed | Support Page', 41 | }, 42 | { 43 | label: 'Settings', 44 | routes: [ 45 | { 46 | element: , 47 | exact: true, 48 | label: 'General', 49 | path: '/settings/general', 50 | title: 'PatternFly Seed | General Settings', 51 | }, 52 | { 53 | element: , 54 | exact: true, 55 | label: 'Profile', 56 | path: '/settings/profile', 57 | title: 'PatternFly Seed | Profile Settings', 58 | }, 59 | ], 60 | }, 61 | ]; 62 | 63 | const flattenedRoutes: IAppRoute[] = routes.reduce( 64 | (flattened, route) => [...flattened, ...(route.routes ? route.routes : [route])], 65 | [] as IAppRoute[], 66 | ); 67 | 68 | const AppRoutes = (): React.ReactElement => ( 69 | 70 | {flattenedRoutes.map(({ path, element }, idx) => ( 71 | 75 | ); 76 | 77 | export { AppRoutes, routes }; 78 | -------------------------------------------------------------------------------- /src/app/utils/useDocumentTitle.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | // a custom hook for setting the page title 4 | export function useDocumentTitle(title: string) { 5 | React.useEffect(() => { 6 | const originalTitle = document.title; 7 | document.title = title; 8 | 9 | return () => { 10 | document.title = originalTitle; 11 | }; 12 | }, [title]); 13 | } 14 | -------------------------------------------------------------------------------- /src/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patternfly/patternfly-react-seed/72362947d44debca429fc1cebb278437627c63a8/src/favicon.png -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |72 | ))} 73 | } /> 74 | Patternfly Seed 7 | 8 | 9 | 10 |11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from '@app/index'; 4 | 5 | if (process.env.NODE_ENV !== "production") { 6 | const config = { 7 | rules: [ 8 | { 9 | id: 'color-contrast', 10 | enabled: false 11 | } 12 | ] 13 | }; 14 | // eslint-disable-next-line @typescript-eslint/no-require-imports 15 | const axe = require("react-axe"); 16 | axe(React, ReactDOM, 1000, config); 17 | } 18 | 19 | const root = ReactDOM.createRoot(document.getElementById("root") as Element); 20 | 21 | root.render( 22 | 23 | 25 | ) 26 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png'; 2 | declare module '*.jpg'; 3 | declare module '*.jpeg'; 4 | declare module '*.gif'; 5 | declare module '*.svg'; 6 | declare module '*.css'; 7 | declare module '*.wav'; 8 | declare module '*.mp3'; 9 | declare module '*.m4a'; 10 | declare module '*.rdf'; 11 | declare module '*.ttl'; 12 | declare module '*.pdf'; 13 | -------------------------------------------------------------------------------- /stylePaths.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | module.exports = { 3 | stylePaths: [ 4 | path.resolve(__dirname, 'src'), 5 | path.resolve(__dirname, 'node_modules/patternfly'), 6 | path.resolve(__dirname, 'node_modules/@patternfly/patternfly'), 7 | path.resolve(__dirname, 'node_modules/@patternfly/react-styles/css'), 8 | path.resolve(__dirname, 'node_modules/@patternfly/react-core/dist/styles/base.css'), 9 | path.resolve(__dirname, 'node_modules/@patternfly/react-core/dist/esm/@patternfly/patternfly'), 10 | path.resolve(__dirname, 'node_modules/@patternfly/react-core/node_modules/@patternfly/react-styles/css'), 11 | path.resolve(__dirname, 'node_modules/@patternfly/react-table/node_modules/@patternfly/react-styles/css'), 12 | path.resolve(__dirname, 'node_modules/@patternfly/react-inline-edit-extension/node_modules/@patternfly/react-styles/css') 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "rootDir": ".", 5 | "outDir": "dist", 6 | "module": "esnext", 7 | "target": "es5", 8 | "lib": ["es6", "dom"], 9 | "sourceMap": true, 10 | "jsx": "react", 11 | "moduleResolution": "node", 12 | "forceConsistentCasingInFileNames": true, 13 | "noImplicitReturns": true, 14 | "noImplicitThis": true, 15 | "noImplicitAny": false, 16 | "allowJs": true, 17 | "esModuleInterop": true, 18 | "allowSyntheticDefaultImports": true, 19 | "strict": true, 20 | "paths": { 21 | "@app/*": ["src/app/*"], 22 | "@assets/*": ["node_modules/@patternfly/react-core/dist/styles/assets/*"] 23 | }, 24 | "importHelpers": true, 25 | "skipLibCheck": true 26 | }, 27 | "include": [ 28 | "**/*.ts", 29 | "**/*.tsx", 30 | "**/*.jsx", 31 | "**/*.js" 32 | ], 33 | "exclude": ["node_modules"] 34 | } 35 | -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | 3 | const path = require('path'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | const CopyPlugin = require('copy-webpack-plugin'); 6 | const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); 7 | const Dotenv = require('dotenv-webpack'); 8 | const BG_IMAGES_DIRNAME = 'bgimages'; 9 | const ASSET_PATH = process.env.ASSET_PATH || '/'; 10 | module.exports = (env) => { 11 | return { 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.(tsx|ts|jsx)?$/, 16 | use: [ 17 | { 18 | loader: 'ts-loader', 19 | options: { 20 | transpileOnly: true, 21 | experimentalWatchApi: true, 22 | }, 23 | }, 24 | ], 25 | }, 26 | { 27 | test: /\.(svg|ttf|eot|woff|woff2)$/, 28 | type: 'asset/resource', 29 | // only process modules with this loader 30 | // if they live under a 'fonts' or 'pficon' directory 31 | include: [ 32 | path.resolve(__dirname, 'node_modules/patternfly/dist/fonts'), 33 | path.resolve(__dirname, 'node_modules/@patternfly/react-core/dist/styles/assets/fonts'), 34 | path.resolve(__dirname, 'node_modules/@patternfly/react-core/dist/styles/assets/pficon'), 35 | path.resolve(__dirname, 'node_modules/@patternfly/patternfly/assets/fonts'), 36 | path.resolve(__dirname, 'node_modules/@patternfly/patternfly/assets/pficon'), 37 | ], 38 | }, 39 | { 40 | test: /\.svg$/, 41 | type: 'asset/inline', 42 | include: (input) => input.indexOf('background-filter.svg') > 1, 43 | use: [ 44 | { 45 | options: { 46 | limit: 5000, 47 | outputPath: 'svgs', 48 | name: '[name].[ext]', 49 | }, 50 | }, 51 | ], 52 | }, 53 | { 54 | test: /\.svg$/, 55 | // only process SVG modules with this loader if they live under a 'bgimages' directory 56 | // this is primarily useful when applying a CSS background using an SVG 57 | include: (input) => input.indexOf(BG_IMAGES_DIRNAME) > -1, 58 | type: 'asset/inline', 59 | }, 60 | { 61 | test: /\.svg$/, 62 | // only process SVG modules with this loader when they don't live under a 'bgimages', 63 | // 'fonts', or 'pficon' directory, those are handled with other loaders 64 | include: (input) => 65 | input.indexOf(BG_IMAGES_DIRNAME) === -1 && 66 | input.indexOf('fonts') === -1 && 67 | input.indexOf('background-filter') === -1 && 68 | input.indexOf('pficon') === -1, 69 | use: { 70 | loader: 'raw-loader', 71 | options: {}, 72 | }, 73 | }, 74 | { 75 | test: /\.(jpg|jpeg|png|gif)$/i, 76 | include: [ 77 | path.resolve(__dirname, 'src'), 78 | path.resolve(__dirname, 'node_modules/patternfly'), 79 | path.resolve(__dirname, 'node_modules/@patternfly/patternfly/assets/images'), 80 | path.resolve(__dirname, 'node_modules/@patternfly/react-styles/css/assets/images'), 81 | path.resolve(__dirname, 'node_modules/@patternfly/react-core/dist/styles/assets/images'), 82 | path.resolve( 83 | __dirname, 84 | 'node_modules/@patternfly/react-core/node_modules/@patternfly/react-styles/css/assets/images' 85 | ), 86 | path.resolve( 87 | __dirname, 88 | 'node_modules/@patternfly/react-table/node_modules/@patternfly/react-styles/css/assets/images' 89 | ), 90 | path.resolve( 91 | __dirname, 92 | 'node_modules/@patternfly/react-inline-edit-extension/node_modules/@patternfly/react-styles/css/assets/images' 93 | ), 94 | ], 95 | type: 'asset/inline', 96 | use: [ 97 | { 98 | options: { 99 | limit: 5000, 100 | outputPath: 'images', 101 | name: '[name].[ext]', 102 | }, 103 | }, 104 | ], 105 | }, 106 | ], 107 | }, 108 | output: { 109 | filename: '[name].bundle.js', 110 | path: path.resolve(__dirname, 'dist'), 111 | publicPath: ASSET_PATH, 112 | }, 113 | plugins: [ 114 | new HtmlWebpackPlugin({ 115 | template: path.resolve(__dirname, 'src', 'index.html'), 116 | }), 117 | new Dotenv({ 118 | systemvars: true, 119 | silent: true, 120 | }), 121 | new CopyPlugin({ 122 | patterns: [{ from: './src/favicon.png', to: 'images' }], 123 | }), 124 | ], 125 | resolve: { 126 | extensions: ['.js', '.ts', '.tsx', '.jsx'], 127 | plugins: [ 128 | new TsconfigPathsPlugin({ 129 | configFile: path.resolve(__dirname, './tsconfig.json'), 130 | }), 131 | ], 132 | symlinks: false, 133 | cacheWithContext: false, 134 | }, 135 | }; 136 | }; 137 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | 3 | const path = require('path'); 4 | const { merge } = require('webpack-merge'); 5 | const common = require('./webpack.common.js'); 6 | const { stylePaths } = require('./stylePaths'); 7 | const HOST = process.env.HOST || 'localhost'; 8 | const PORT = process.env.PORT || '9000'; 9 | 10 | module.exports = merge(common('development'), { 11 | mode: 'development', 12 | devtool: 'eval-source-map', 13 | devServer: { 14 | host: HOST, 15 | port: PORT, 16 | historyApiFallback: true, 17 | open: true, 18 | static: { 19 | directory: path.resolve(__dirname, 'dist'), 20 | }, 21 | client: { 22 | overlay: true, 23 | }, 24 | }, 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.css$/, 29 | include: [...stylePaths], 30 | use: ['style-loader', 'css-loader'], 31 | }, 32 | ], 33 | }, 34 | }); 35 | -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | 3 | const { merge } = require('webpack-merge'); 4 | const common = require('./webpack.common.js'); 5 | const { stylePaths } = require('./stylePaths'); 6 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 7 | const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); 8 | const TerserJSPlugin = require('terser-webpack-plugin'); 9 | 10 | module.exports = merge(common('production'), { 11 | mode: 'production', 12 | devtool: 'source-map', 13 | optimization: { 14 | minimizer: [ 15 | new TerserJSPlugin({}), 16 | new CssMinimizerPlugin({ 17 | minimizerOptions: { 18 | preset: ['default', { mergeLonghand: false }], 19 | }, 20 | }), 21 | ], 22 | }, 23 | plugins: [ 24 | new MiniCssExtractPlugin({ 25 | filename: '[name].css', 26 | chunkFilename: '[name].bundle.css', 27 | }), 28 | ], 29 | module: { 30 | rules: [ 31 | { 32 | test: /\.css$/, 33 | include: [...stylePaths], 34 | use: [MiniCssExtractPlugin.loader, 'css-loader'], 35 | }, 36 | ], 37 | }, 38 | }); 39 | --------------------------------------------------------------------------------24 |