diff --git a/README.md b/README.md index b76a7e5b..cd74b877 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Please reference [PatternFly's AI-assisted development guidelines](https://githu ### Before adding a new component: - make sure your use case is new/complex enough to be added to this extension -- the component should bring a value value above and beyond existing PatternFly components +- the component should bring a value above and beyond existing PatternFly components ### To add a new component: @@ -40,7 +40,7 @@ Please reference [PatternFly's AI-assisted development guidelines](https://githu #### Example component: -``` +``` TSX import * as React from 'react'; import { Content } from '@patternfly/react-core'; import { createUseStyles } from 'react-jss'; @@ -75,7 +75,7 @@ export default MyComponent; #### Index file example: -``` +``` TSX export { default } from './MyComponent'; export * from './MyComponent'; ``` @@ -99,8 +99,7 @@ src #### Component API definition example: -``` - +``` TSX import { FunctionComponent } from 'react'; // when possible, extend available PatternFly types @@ -113,7 +112,7 @@ export const MyComponent: FunctionComponent = ({ customLabel, #### Markdown file example: -```` +```` MDX --- section: Component groups subsection: My component's category @@ -135,7 +134,7 @@ MyComponent has been created to demo contributing to this repository. #### Component usage file example: (`MyComponentExample.tsx`) -``` +``` TSX import { FunctionComponent } from 'react'; const MyComponentExample: FunctionComponent = () => ( diff --git a/package-lock.json b/package-lock.json index 72358f3c..efad1fcf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "packages/*" ], "dependencies": { - "@patternfly/react-drag-drop": "^6.0.0", + "@patternfly/react-drag-drop": "^6.6.2", "@patternfly/react-tokens": "^6.0.0", "sharp": "^0.34.0" }, @@ -5422,22 +5422,22 @@ } }, "node_modules/@patternfly/react-code-editor": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/@patternfly/react-code-editor/-/react-code-editor-6.2.2.tgz", - "integrity": "sha512-KPnkNP769afD2rvoNQtgCx+SYscamM5QSRmw2FJ9QPHVMksarwTsMvrdMxvu+n6Dhs/T40vQLU5UR7X2yPrURg==", + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/@patternfly/react-code-editor/-/react-code-editor-6.6.2.tgz", + "integrity": "sha512-opy2UXmSWRJUnssuqNzi5kMxjGyck6n78YCx3sjFLO6uosrSGRJEggQnRlot3jw4/+QmOHBWThlI5lrYpjIInw==", "dev": true, "license": "MIT", "dependencies": { - "@monaco-editor/react": "^4.6.0", - "@patternfly/react-core": "^6.2.2", - "@patternfly/react-icons": "^6.2.2", - "@patternfly/react-styles": "^6.2.2", + "@monaco-editor/react": "^4.7.0", + "@patternfly/react-core": "^6.6.2", + "@patternfly/react-icons": "^6.6.1", + "@patternfly/react-styles": "^6.6.1", "react-dropzone": "14.3.5", "tslib": "^2.8.1" }, "peerDependencies": { - "react": "^17 || ^18", - "react-dom": "^17 || ^18" + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" } }, "node_modules/@patternfly/react-component-groups": { @@ -5445,15 +5445,15 @@ "link": true }, "node_modules/@patternfly/react-core": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-6.3.1.tgz", - "integrity": "sha512-1qV20nU4M6PA28qnikH9fPLQlkteaZZToFlATjBNBw7aUI6zIvj7U0akkHz8raWcfHAI+tAzGV7dfKjiv035/g==", + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-6.6.2.tgz", + "integrity": "sha512-FjXUSH+zNUto2Qc6t62nW9aEYdy6n+7njtAcZGFb3HhifMtufzOXGg8jNe4rrBAEYBroB913+xoaaQoK5CUwBA==", "license": "MIT", "dependencies": { - "@patternfly/react-icons": "^6.3.1", - "@patternfly/react-styles": "^6.3.1", - "@patternfly/react-tokens": "^6.3.1", - "focus-trap": "7.6.4", + "@patternfly/react-icons": "^6.6.1", + "@patternfly/react-styles": "^6.6.1", + "@patternfly/react-tokens": "^6.6.1", + "focus-trap": "7.6.6", "react-dropzone": "^14.3.5", "tslib": "^2.8.1" }, @@ -5463,17 +5463,17 @@ } }, "node_modules/@patternfly/react-drag-drop": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/@patternfly/react-drag-drop/-/react-drag-drop-6.3.1.tgz", - "integrity": "sha512-lTPTSCtScYm+5NPCbr8hmSMOggOEhvIIzsyoVWF/G+iJBR97u0fdvsBqRvTg95hv2R/bKTXHigSCgixqnE9XdQ==", + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/@patternfly/react-drag-drop/-/react-drag-drop-6.6.2.tgz", + "integrity": "sha512-JBOF3By3bBnsW7/pogkFbHLHeLh+xSOtHkLFdqjfhDtmOOM0qhdz/fzzRcM3hqYtMFVwj0MiL9m5H2YolnP68g==", "license": "MIT", "dependencies": { "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", - "@patternfly/react-core": "^6.3.1", - "@patternfly/react-icons": "^6.3.1", - "@patternfly/react-styles": "^6.3.1", + "@patternfly/react-core": "^6.6.2", + "@patternfly/react-icons": "^6.6.1", + "@patternfly/react-styles": "^6.6.1", "resize-observer-polyfill": "^1.5.1" }, "peerDependencies": { @@ -5482,43 +5482,46 @@ } }, "node_modules/@patternfly/react-icons": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-6.3.1.tgz", - "integrity": "sha512-uiMounSIww1iZLM4pq+X8c3upzwl9iowXRPjR5CA8entb70lwgAXg3PqvypnuTAcilTq1Y3k5sFTqkhz7rgKcQ==", + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-6.6.1.tgz", + "integrity": "sha512-EUHnTliMlUMNfaueyGTaNcdCDGN4CjLtQpNdSg6iWcl5oloHtUDEAIltP0nbCWnepMJDoM2YgjMlyjwWlkNzXg==", "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + }, "peerDependencies": { "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" } }, "node_modules/@patternfly/react-styles": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-6.3.1.tgz", - "integrity": "sha512-hyb+PlO8YITjKh2wBvjdeZhX6FyB3hlf4r6yG4rPOHk4SgneXHjNSdGwQ3szAxgGqtbENCYtOqwD/8ai72GrxQ==", + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-6.6.1.tgz", + "integrity": "sha512-hwByXA4wSpOIBJFtrYUdkFDIqvzHRP5yzvip/NHDlG0y/kXausOh5Z4m00SbTA6t7Hzudw5eU1lf+IOPCm5C6Q==", "license": "MIT" }, "node_modules/@patternfly/react-table": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/@patternfly/react-table/-/react-table-6.2.2.tgz", - "integrity": "sha512-7CxVKhnpA+f8dLJ0hVvzZOe4Djx/nE+w70ipeAHf4Yi5JwfDWmbK97YvjYPfamp/bsXTLtPcK2n4AoY5DQX6Pg==", + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/@patternfly/react-table/-/react-table-6.6.2.tgz", + "integrity": "sha512-HIxECFB1eiVcAi/DP+FYQMzA7JU4zTg/7idVKuEvWJfILx66ZBwpitwOt27IPxQ3AjCgixLMyh6lTDFAfmd3Kw==", "license": "MIT", "dependencies": { - "@patternfly/react-core": "^6.2.2", - "@patternfly/react-icons": "^6.2.2", - "@patternfly/react-styles": "^6.2.2", - "@patternfly/react-tokens": "^6.2.2", - "lodash": "^4.17.21", + "@patternfly/react-core": "^6.6.2", + "@patternfly/react-icons": "^6.6.1", + "@patternfly/react-styles": "^6.6.1", + "@patternfly/react-tokens": "^6.6.1", + "lodash": "^4.18.1", "tslib": "^2.8.1" }, "peerDependencies": { - "react": "^17 || ^18", - "react-dom": "^17 || ^18" + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" } }, "node_modules/@patternfly/react-tokens": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-6.3.1.tgz", - "integrity": "sha512-wt/xKU1tGCDXUueFb+8/Cwxlm4vUD/Xl26O8MxbSLm6NZAHOUPwytJ7gugloGSPvc/zcsXxEgKANL8UZNO6DTw==", + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-6.6.1.tgz", + "integrity": "sha512-t7dDEJMnO3QkdIKyjbyKJznVYki9ONZG27Cn/9RXwaM0lDGCrgQEJXy2XoVUCdYhIdXZu+TGSEqXeNFYUyZECg==", "license": "MIT" }, "node_modules/@pkgjs/parseargs": { @@ -14051,12 +14054,12 @@ } }, "node_modules/focus-trap": { - "version": "7.6.4", - "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.4.tgz", - "integrity": "sha512-xx560wGBk7seZ6y933idtjJQc1l+ck+pI3sKvhKozdBV1dRZoKhkW5xoCaFv9tQiX5RH1xfSxjuNu6g+lmN/gw==", + "version": "7.6.6", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.6.tgz", + "integrity": "sha512-v/Z8bvMCajtx4mEXmOo7QEsIzlIOqRXTIwgUfsFOF9gEsespdbD0AkPIka1bSXZ8Y8oZ+2IVDQZePkTfEHZl7Q==", "license": "MIT", "dependencies": { - "tabbable": "^6.2.0" + "tabbable": "^6.3.0" } }, "node_modules/follow-redirects": { @@ -25680,9 +25683,9 @@ } }, "node_modules/tabbable": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", - "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.5.0.tgz", + "integrity": "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==", "license": "MIT" }, "node_modules/tapable": { @@ -29021,17 +29024,17 @@ "version": "5.0.0-prerelease.0", "license": "MIT", "dependencies": { - "@patternfly/react-core": "^6.0.0", + "@patternfly/react-core": "^6.6.2", "@patternfly/react-icons": "^6.0.0", "@patternfly/react-styles": "^6.0.0", - "@patternfly/react-table": "^6.0.0", + "@patternfly/react-table": "^6.6.2", "react-jss": "^10.10.0" }, "devDependencies": { "@patternfly/documentation-framework": "^6.5.16", "@patternfly/patternfly": "^6.0.0", "@patternfly/patternfly-a11y": "^5.1.0", - "@patternfly/react-code-editor": "^6.0.0", + "@patternfly/react-code-editor": "^6.6.2", "@types/react": "^18.2.33", "@types/react-dom": "^18.3.1", "react": "^18.3.1", @@ -29039,7 +29042,7 @@ "typescript": "^5.8.3" }, "peerDependencies": { - "@patternfly/react-drag-drop": "^6.0.0", + "@patternfly/react-drag-drop": "^6.6.2", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" } diff --git a/package.json b/package.json index c52a5a6b..9ad47556 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "whatwg-fetch": "^3.6.20" }, "dependencies": { - "@patternfly/react-drag-drop": "^6.0.0", + "@patternfly/react-drag-drop": "^6.6.2", "@patternfly/react-tokens": "^6.0.0", "sharp": "^0.34.0" } diff --git a/packages/module/generate-fed-package-json.js b/packages/module/generate-fed-package-json.js index c14056fa..42933f14 100644 --- a/packages/module/generate-fed-package-json.js +++ b/packages/module/generate-fed-package-json.js @@ -1,14 +1,14 @@ const fse = require('fs-extra'); -const { globSync } = require('glob'); const path = require('path'); const { default: getDynamicModuleMap } = require('../../scripts/parse-dynamic-modules.mjs'); +const { toPosixPath, posixGlobSync, posixRelative } = require('./utils'); -const root = process.cwd(); +const root = toPosixPath(process.cwd()); -const sourceFiles = globSync(`${root}/src/*/`) +const sourceFiles = posixGlobSync(`${root}/src/*/`) .map((name) => name.replace(/\/$/, '')); - -const indexTypings = globSync(`${root}/src/index.d.ts`); + +const indexTypings = posixGlobSync(`${root}/src/index.d.ts`); const ENV_AGNOSTIC_ROOT = `${root}/dist/dynamic` @@ -23,9 +23,9 @@ async function copyTypings(files, dest) { async function createPackage(file) { const fileName = file.split('/').pop(); - const esmSource = globSync(`${root}/dist/esm/${fileName}/**/index.js`)[0]; - const cjsSource = globSync(`${root}/dist/cjs/${fileName}/**/index.js`)[0]; - const typingsSource = globSync(`${root}/dist/esm/${fileName}/**/index.d.ts`)[0] + const esmSource = posixGlobSync(`${root}/dist/esm/${fileName}/**/index.js`)[0]; + const cjsSource = posixGlobSync(`${root}/dist/cjs/${fileName}/**/index.js`)[0]; + const typingsSource = posixGlobSync(`${root}/dist/esm/${fileName}/**/index.d.ts`)[0] /** * Prevent creating package.json for directories with no JS files (like CSS directories) */ @@ -39,14 +39,14 @@ async function createPackage(file) { // ensure the directory exists fse.ensureDirSync(destDir) - const esmRelative = path.relative(file, esmSource).replace('/dist', ''); - const cjsRelative = path.relative(file, cjsSource).replace('/dist', ''); - const tsRelative = path.relative(file, typingsSource).replace('/dist', '') + const esmRelative = posixRelative(file, esmSource).replace('/dist', ''); + const cjsRelative = posixRelative(file, cjsSource).replace('/dist', ''); + const tsRelative = posixRelative(file, typingsSource).replace('/dist', '') const content = { main: cjsRelative, module: esmRelative, }; - const typings = globSync(`${root}/src/${fileName}/*.d.ts`); + const typings = posixGlobSync(`${root}/src/${fileName}/*.d.ts`); const cmds = []; content.typings = tsRelative; cmds.push(copyTypings(typings, `${root}/dist/${fileName}`)); diff --git a/packages/module/generate-index.js b/packages/module/generate-index.js index 8ff8b416..3afa0b06 100644 --- a/packages/module/generate-index.js +++ b/packages/module/generate-index.js @@ -1,12 +1,12 @@ const fse = require('fs-extra'); -const { globSync } = require('glob'); const path = require('path'); +const { posixGlobSync } = require('./utils'); const root = process.cwd(); const ENV_AGNOSTIC_ROOT = `${root}/src` -const sourceFiles = globSync(path.resolve(__dirname, './src/*/index.ts')) +const sourceFiles = posixGlobSync(path.resolve(__dirname, './src/*/index.ts')) async function generateIndex(files) { // ensure the dynamic root exists diff --git a/packages/module/package.json b/packages/module/package.json index e251230c..adc780b0 100644 --- a/packages/module/package.json +++ b/packages/module/package.json @@ -31,22 +31,22 @@ "tag": "alpha" }, "dependencies": { - "@patternfly/react-core": "^6.0.0", + "@patternfly/react-core": "^6.6.2", "@patternfly/react-icons": "^6.0.0", "@patternfly/react-styles": "^6.0.0", - "@patternfly/react-table": "^6.0.0", + "@patternfly/react-table": "^6.6.2", "react-jss": "^10.10.0" }, "peerDependencies": { - "@patternfly/react-drag-drop": "^6.0.0", + "@patternfly/react-drag-drop": "^6.6.2", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "devDependencies": { - "@patternfly/patternfly-a11y": "^5.1.0", "@patternfly/documentation-framework": "^6.5.16", - "@patternfly/react-code-editor": "^6.0.0", "@patternfly/patternfly": "^6.0.0", + "@patternfly/patternfly-a11y": "^5.1.0", + "@patternfly/react-code-editor": "^6.6.2", "@types/react": "^18.2.33", "@types/react-dom": "^18.3.1", "react": "^18.3.1", diff --git a/packages/module/patternfly-docs/content/extensions/component-groups/examples/Tearsheet/Tearsheet.md b/packages/module/patternfly-docs/content/extensions/component-groups/examples/Tearsheet/Tearsheet.md new file mode 100644 index 00000000..43be9bfc --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/component-groups/examples/Tearsheet/Tearsheet.md @@ -0,0 +1,52 @@ +--- +section: extensions +subsection: component-groups +id: Tearsheet +source: react +propComponents: ['Tearsheet', 'TearsheetHeader', 'TearsheetBody', 'TearsheetFooter', 'TearsheetGroup'] +sourceLink: https://github.com/patternfly/react-component-groups/blob/main/packages/module/patternfly-docs/content/extensions/component-groups/examples/Tearsheet/Tearsheet.md +--- + +import { Fragment, useState } from 'react'; +import spacing from '@patternfly/react-styles/css/utilities/Spacing/spacing'; +import TearsheetGroup from '@patternfly/react-component-groups/dist/dynamic/TearsheetGroup'; +import Tearsheet from '@patternfly/react-component-groups/dist/dynamic/Tearsheet'; +import TearsheetHeader from '@patternfly/react-component-groups/dist/dynamic/TearsheetHeader'; +import TearsheetBody from '@patternfly/react-component-groups/dist/dynamic/TearsheetBody'; +import TearsheetFooter from '@patternfly/react-component-groups/dist/dynamic/TearsheetFooter'; + +**Tearsheet** is an extension of the [modal component](/components/modal), allowing more complex experiences to be provided to the user. Additionally, while the biggest `` size (`ModalVariant.large`) may work for some cases, tearsheet allows more of the viewport area to be utilized. + +## Examples + +### Basic + +Basic tearsheets should make use of the entire container. For this basic example, heading and body text is rendered with an action list placed within the footer area. + +```ts file="./TearsheetBasic.tsx" +``` + +### Tearsheet layouts + +Tearsheets can be customized to render various layout styles. These layout styles include, [full width text](/foundations-and-styles/utility-classes/alignment), [flex](/foundations-and-styles/layouts/flex), and [grid](/foundations-and-styles/layouts/grid). + +The `` component will handle scrolling for long content. + +```ts file="./TearsheetLayouts.tsx" +``` + +### Stacked + +Tearsheets support stacking, allowing new sheets to open on top of active ones. They utilize three visible stack levels (0, 1, and 2) and a background level (-1) to hide inactive sheets. + +Limit stacked tearsheets to a maximum of 3. Flows requiring 4 or more levels should be redesigned using multi-step wizard or dedicates page to prevent loss of user context. + +```ts file="./TearsheetStacked.tsx" +``` + +### Tearsheets vs Modals + +Choose a tearsheet over a modal when users need to process detailed and complex workflows. This example demonstrates how the expanded surface area of a tearsheet allows users to easily navigate a robust layout without feeling overwhelmed. + +```ts file="./TearsheetComparison.tsx" +``` diff --git a/packages/module/patternfly-docs/content/extensions/component-groups/examples/Tearsheet/TearsheetBasic.tsx b/packages/module/patternfly-docs/content/extensions/component-groups/examples/Tearsheet/TearsheetBasic.tsx new file mode 100644 index 00000000..e25e1655 --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/component-groups/examples/Tearsheet/TearsheetBasic.tsx @@ -0,0 +1,43 @@ +import { Fragment, useState } from 'react'; +import { Button } from '@patternfly/react-core'; +import Tearsheet from '@patternfly/react-component-groups/dist/dynamic/Tearsheet'; +import TearsheetHeader from '@patternfly/react-component-groups/dist/dynamic/TearsheetHeader'; +import TearsheetBody from '@patternfly/react-component-groups/dist/dynamic/TearsheetBody'; +import TearsheetFooter from '@patternfly/react-component-groups/dist/dynamic/TearsheetFooter'; + +export const TearsheetBasic: React.FunctionComponent = () => { + const [ isTearsheetOpen, setIsTearsheetOpen ] = useState(false); + + const toggleTearsheet = (_event: React.MouseEvent | KeyboardEvent | MouseEvent) => { + setIsTearsheetOpen(!isTearsheetOpen); + }; + + return ( + + + | KeyboardEvent | MouseEvent) => toggleTearsheet(e)} + > + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore + magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. + + + + + + + + ); +}; diff --git a/packages/module/patternfly-docs/content/extensions/component-groups/examples/Tearsheet/TearsheetComparison.tsx b/packages/module/patternfly-docs/content/extensions/component-groups/examples/Tearsheet/TearsheetComparison.tsx new file mode 100644 index 00000000..b756aa4c --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/component-groups/examples/Tearsheet/TearsheetComparison.tsx @@ -0,0 +1,257 @@ +import { Fragment, useState } from 'react'; +import { + Button, + Card, + CardBody, + CardHeader, + CardTitle, + CodeBlock, + CodeBlockCode, + Content, + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + Flex, + Grid, + GridItem, + JumpLinks, + JumpLinksItem, + Label, + LabelGroup, + Modal, + ModalBody, + ModalFooter, + ModalHeader, + ModalVariant, + PageSection, + SearchInput, + Sidebar, + SidebarContent, + SidebarPanel +} from '@patternfly/react-core'; +import Tearsheet from '@patternfly/react-component-groups/dist/dynamic/Tearsheet'; +import TearsheetHeader from '@patternfly/react-component-groups/dist/dynamic/TearsheetHeader'; +import TearsheetBody from '@patternfly/react-component-groups/dist/dynamic/TearsheetBody'; +import TearsheetFooter from '@patternfly/react-component-groups/dist/dynamic/TearsheetFooter'; + +const SIDEBAR_FIX_CLASS = 'tearsheet-comparison-sidebar'; +const sidebarFixStyles = ` + .${SIDEBAR_FIX_CLASS} .pf-v6-c-sidebar__main { height: 100%; } + .${SIDEBAR_FIX_CLASS} .pf-v6-c-sidebar__content { overflow: scroll; height: 100%; } +`; + +const LOREM = + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore ' + + 'magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo ' + + 'consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla ' + + 'pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id ' + + 'est laborum.'; + +const sections = [ + { + id: 'overview', + title: 'Overview', + description: 'High-level summary of the resource, including its purpose, current state, and key metadata.', + labels: [ 'v1', 'stable', 'core' ], + items: [ + { name: 'ConfigMap', status: 'Active', created: '2025-06-01', owner: 'platform-team' }, + { name: 'Secret', status: 'Active', created: '2025-06-02', owner: 'security-team' }, + { name: 'ServiceAccount', status: 'Active', created: '2025-05-28', owner: 'platform-team' }, + { name: 'Namespace', status: 'Terminating', created: '2025-04-15', owner: 'admin' }, + { name: 'LimitRange', status: 'Active', created: '2025-06-03', owner: 'ops-team' }, + { name: 'ResourceQuota', status: 'Active', created: '2025-06-03', owner: 'ops-team' } + ], + code: `apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: my-config\n namespace: default\n labels:\n app.kubernetes.io/name: my-app\n app.kubernetes.io/version: "1.2.0"\n app.kubernetes.io/managed-by: helm\ndata:\n APP_ENV: production\n LOG_LEVEL: info\n MAX_CONNECTIONS: "100"` + }, + { + id: 'configuration', + title: 'Configuration', + description: 'Runtime parameters, feature flags, and environment-specific settings that control application behavior.', + labels: [ 'apps/v1', 'deployment', 'rolling-update' ], + items: [ + { name: 'Deployment', status: 'Available', created: '2025-06-01', owner: 'dev-team' }, + { name: 'StatefulSet', status: 'Ready', created: '2025-06-01', owner: 'data-team' }, + { name: 'DaemonSet', status: 'Scheduled', created: '2025-05-30', owner: 'infra-team' }, + { name: 'ReplicaSet', status: 'Available', created: '2025-06-01', owner: 'dev-team' }, + { name: 'CronJob', status: 'Suspended', created: '2025-05-20', owner: 'batch-team' }, + { name: 'Job', status: 'Complete', created: '2025-06-04', owner: 'batch-team' } + ], + code: `spec:\n replicas: 3\n selector:\n matchLabels:\n app: my-app\n strategy:\n type: RollingUpdate\n rollingUpdate:\n maxSurge: 1\n maxUnavailable: 0\n template:\n spec:\n containers:\n - name: app\n image: registry.io/my-app:1.2.0\n env:\n - name: DB_HOST\n valueFrom:\n secretKeyRef:\n name: db-credentials\n key: host` + }, + { + id: 'resources', + title: 'Resources', + description: 'CPU, memory, and storage allocations for each container in the workload.', + labels: [ 'requests', 'limits', 'QoS: Burstable' ], + items: [ + { name: 'app', status: 'Running', created: '2025-06-01', owner: 'dev-team' }, + { name: 'sidecar-proxy', status: 'Running', created: '2025-06-01', owner: 'mesh-team' }, + { name: 'log-collector', status: 'Running', created: '2025-06-01', owner: 'observability' }, + { name: 'init-db', status: 'Completed', created: '2025-06-01', owner: 'dev-team' }, + { name: 'init-config', status: 'Completed', created: '2025-06-01', owner: 'platform-team' }, + { name: 'debug', status: 'Waiting', created: '2025-06-05', owner: 'sre-team' } + ], + code: `containers:\n - name: app\n resources:\n requests:\n cpu: "250m"\n memory: "512Mi"\n ephemeral-storage: "1Gi"\n limits:\n cpu: "1"\n memory: "1Gi"\n ephemeral-storage: "2Gi"\n - name: sidecar-proxy\n resources:\n requests:\n cpu: "100m"\n memory: "128Mi"\n limits:\n cpu: "200m"\n memory: "256Mi"` + }, + { + id: 'networking', + title: 'Networking', + description: 'Service exposure, ingress rules, and network policies governing traffic flow.', + labels: [ 'ClusterIP', 'Ingress', 'NetworkPolicy' ], + items: [ + { name: 'my-service', status: 'Active', created: '2025-06-01', owner: 'dev-team' }, + { name: 'my-service-headless', status: 'Active', created: '2025-06-01', owner: 'data-team' }, + { name: 'ingress-main', status: 'Synced', created: '2025-06-02', owner: 'platform-team' }, + { name: 'netpol-deny-all', status: 'Enforcing', created: '2025-05-15', owner: 'security-team' }, + { name: 'netpol-allow-web', status: 'Enforcing', created: '2025-05-15', owner: 'security-team' }, + { name: 'external-dns', status: 'Active', created: '2025-06-03', owner: 'infra-team' } + ], + code: `apiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n name: my-ingress\n annotations:\n nginx.ingress.kubernetes.io/rewrite-target: /\n cert-manager.io/cluster-issuer: letsencrypt\nspec:\n tls:\n - hosts:\n - app.example.com\n secretName: app-tls\n rules:\n - host: app.example.com\n http:\n paths:\n - path: /api\n pathType: Prefix\n backend:\n service:\n name: my-service\n port:\n number: 8080` + }, + { + id: 'storage', + title: 'Storage', + description: 'Persistent volume claims, storage classes, and mount configurations.', + labels: [ 'gp3', 'ReadWriteOnce', 'Retain' ], + items: [ + { name: 'data-pvc', status: 'Bound', created: '2025-06-01', owner: 'data-team' }, + { name: 'logs-pvc', status: 'Bound', created: '2025-06-01', owner: 'observability' }, + { name: 'backup-pvc', status: 'Bound', created: '2025-05-20', owner: 'ops-team' }, + { name: 'tmp-pvc', status: 'Pending', created: '2025-06-05', owner: 'dev-team' }, + { name: 'cache-emptydir', status: 'Mounted', created: '2025-06-01', owner: 'dev-team' }, + { name: 'config-projected', status: 'Mounted', created: '2025-06-01', owner: 'platform-team' } + ], + code: `apiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n name: data-pvc\nspec:\n accessModes:\n - ReadWriteOnce\n storageClassName: gp3-encrypted\n resources:\n requests:\n storage: 50Gi\n---\nvolumeMounts:\n - name: data\n mountPath: /var/lib/data\n - name: logs\n mountPath: /var/log/app\n - name: cache\n mountPath: /tmp/cache\nvolumes:\n - name: data\n persistentVolumeClaim:\n claimName: data-pvc\n - name: cache\n emptyDir:\n sizeLimit: 500Mi` + }, + { + id: 'monitoring', + title: 'Monitoring', + description: 'Health checks, readiness probes, and metrics endpoints used for observability.', + labels: [ 'prometheus', 'liveness', 'readiness' ], + items: [ + { name: 'liveness-http', status: 'Passing', created: '2025-06-01', owner: 'dev-team' }, + { name: 'readiness-http', status: 'Passing', created: '2025-06-01', owner: 'dev-team' }, + { name: 'startup-tcp', status: 'Passing', created: '2025-06-01', owner: 'dev-team' }, + { name: 'metrics-endpoint', status: 'Scraping', created: '2025-06-02', owner: 'observability' }, + { name: 'alert-high-cpu', status: 'Firing', created: '2025-05-10', owner: 'sre-team' }, + { name: 'alert-error-rate', status: 'Pending', created: '2025-05-10', owner: 'sre-team' } + ], + code: `livenessProbe:\n httpGet:\n path: /healthz\n port: 8080\n initialDelaySeconds: 15\n periodSeconds: 10\n failureThreshold: 3\nreadinessProbe:\n httpGet:\n path: /readyz\n port: 8080\n initialDelaySeconds: 5\n periodSeconds: 5\nstartupProbe:\n tcpSocket:\n port: 8080\n failureThreshold: 30\n periodSeconds: 2\n---\napiVersion: monitoring.coreos.com/v1\nkind: ServiceMonitor\nmetadata:\n name: my-app-monitor\nspec:\n selector:\n matchLabels:\n app: my-app\n endpoints:\n - port: metrics\n interval: 15s\n path: /metrics` + } +]; + +const renderBodyContent = (sidebarClassName = '') => ( + + +
+ + + + {sections.map((s) => ( + e.preventDefault()}> + {s.title} + + ))} + + + + + {sections.map((section) => ( + + + + {section.title} + + {section.labels.map((l) => ( + + ))} + + + {section.description} + + {section.items.map((item, i) => ( + + + + {item.name} + + + + + Status + {item.status} + + + Created + {item.created} + + + Owner + {item.owner} + + + {LOREM.slice(0, 120)} + + {section.code} + + + + + ))} + + ))} + + + +
+
+); + +export const TearsheetComparison: React.FunctionComponent = () => { + const [ isTearsheetOpen, setIsTearsheetOpen ] = useState(false); + const [ isModalOpen, setIsModalOpen ] = useState(false); + + const closeTearsheet = () => setIsTearsheetOpen(false); + const closeModal = () => setIsModalOpen(false); + + return ( + + + + + + + + + + {renderBodyContent(SIDEBAR_FIX_CLASS)} + + + + + + + + + {renderBodyContent()} + + + + + + + ); +}; diff --git a/packages/module/patternfly-docs/content/extensions/component-groups/examples/Tearsheet/TearsheetLayouts.tsx b/packages/module/patternfly-docs/content/extensions/component-groups/examples/Tearsheet/TearsheetLayouts.tsx new file mode 100644 index 00000000..33095bf7 --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/component-groups/examples/Tearsheet/TearsheetLayouts.tsx @@ -0,0 +1,117 @@ +import { Fragment, useState } from 'react'; +import { + Button, + Card, + CardBody, + CardHeader, + CardTitle, + Content, + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + Divider, + Title +} from '@patternfly/react-core'; +import Tearsheet from '@patternfly/react-component-groups/dist/dynamic/Tearsheet'; +import TearsheetHeader from '@patternfly/react-component-groups/dist/dynamic/TearsheetHeader'; +import TearsheetBody from '@patternfly/react-component-groups/dist/dynamic/TearsheetBody'; +import TearsheetFooter from '@patternfly/react-component-groups/dist/dynamic/TearsheetFooter'; +import { Flex, FlexItem, Grid, GridItem } from '@patternfly/react-core'; + +type BodyLayout = 'long-text' | 'grid' | 'long'; + +const LOREM = + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore ' + + 'magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo ' + + 'consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla ' + + 'pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id ' + + 'est laborum.'; + +const bodyLayouts: Record React.ReactNode }> = { + 'long-text': { + label: 'Long text', + render: () => ( + <> + {Array.from({ length: 30 }, (_, i) => ( + + {LOREM} + + ))} + + ) + }, + grid: { + label: 'Grid layout', + render: () => ( + + {[ 'Overview', 'Configuration', 'Resources', 'Networking', 'Storage', 'Monitoring' ] + .map((title) => new Array(10).fill(title)) + .flat() + .sort(() => Math.random() - 0.5) + .map((title) => ( + + + + {title} + + {LOREM.slice(0, 120)}... + + + ))} + + ) + }, + long: { + label: 'Flex layout', + render: () => ( + + {[ 'General', 'Details', 'Configuration', 'Permissions', 'Audit log' ].map((section) => ( + + {section} + + + {[ 'Name', 'Status', 'Created', 'Modified' ].map((term) => ( + + {term} + {LOREM.slice(0, 80)} + + ))} + + + ))} + + ) + } +}; + +export const TearsheetLayouts: React.FunctionComponent = () => { + const [ activeLayout, setActiveLayout ] = useState(null); + + const open = (layout: BodyLayout) => () => setActiveLayout(layout); + const close = (_event: React.MouseEvent | KeyboardEvent | MouseEvent) => setActiveLayout(null); + + return ( + + + {(Object.keys(bodyLayouts) as BodyLayout[]).map((key) => ( + + ))} + + + + {activeLayout && bodyLayouts[activeLayout].render()} + + + + + + + ); +}; diff --git a/packages/module/patternfly-docs/content/extensions/component-groups/examples/Tearsheet/TearsheetStacked.tsx b/packages/module/patternfly-docs/content/extensions/component-groups/examples/Tearsheet/TearsheetStacked.tsx new file mode 100644 index 00000000..c2f35398 --- /dev/null +++ b/packages/module/patternfly-docs/content/extensions/component-groups/examples/Tearsheet/TearsheetStacked.tsx @@ -0,0 +1,101 @@ +import { Fragment, useState } from 'react'; +import { Button } from '@patternfly/react-core'; +import Tearsheet from '@patternfly/react-component-groups/dist/dynamic/Tearsheet'; +import TearsheetHeader from '@patternfly/react-component-groups/dist/dynamic/TearsheetHeader'; +import TearsheetBody from '@patternfly/react-component-groups/dist/dynamic/TearsheetBody'; +import TearsheetFooter from '@patternfly/react-component-groups/dist/dynamic/TearsheetFooter'; + +export const TearsheetStacked: React.FunctionComponent = () => { + const [ isTearsheetOpen, setIsTearsheetOpen ] = useState(false); + const [ isStack1TearsheetOpen, setIsStack1TearsheetOpen ] = useState(false); + const [ isStack2TearsheetOpen, setIsStack2TearsheetOpen ] = useState(false); + + const toggleTearsheet = (_event: React.MouseEvent | KeyboardEvent | MouseEvent) => { + setIsTearsheetOpen(!isTearsheetOpen); + }; + const toggleStack1Tearsheet = (_event: React.MouseEvent | KeyboardEvent | MouseEvent) => { + setIsStack1TearsheetOpen(!isStack1TearsheetOpen); + }; + const toggleStack2Tearsheet = (_event: React.MouseEvent | KeyboardEvent | MouseEvent) => { + setIsStack2TearsheetOpen(!isStack2TearsheetOpen); + }; + + return ( + + + | KeyboardEvent | MouseEvent) => toggleTearsheet(e)} + > + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore + magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. + + + + + + + + | KeyboardEvent | MouseEvent) => toggleStack1Tearsheet(e)} + > + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore + magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. + + + + + + + + | KeyboardEvent | MouseEvent) => toggleStack2Tearsheet(e)} + > + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore + magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo + consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id + est laborum. + + + + + + + + ); +}; diff --git a/packages/module/patternfly-docs/patternfly-docs.source.js b/packages/module/patternfly-docs/patternfly-docs.source.js index cff7aac9..f0127391 100644 --- a/packages/module/patternfly-docs/patternfly-docs.source.js +++ b/packages/module/patternfly-docs/patternfly-docs.source.js @@ -1,14 +1,15 @@ const path = require('path'); +const { toPosixPath } = require('../utils'); module.exports = (sourceMD, sourceProps) => { // Parse source content for props so that we can display them const propsIgnore = [ '**/*.test.tsx', '**/examples/*.tsx' ]; const extensionPath = path.join(__dirname, '../src'); - sourceProps(path.join(extensionPath, '/**/*.tsx'), propsIgnore); + sourceProps(toPosixPath(path.join(extensionPath, '/**/*.tsx')), propsIgnore); // Parse md files const contentBase = path.join(__dirname, './content'); - sourceMD(path.join(contentBase, 'extensions/**/*.md'), 'extensions'); + sourceMD(toPosixPath(path.join(contentBase, 'extensions/**/*.md')), 'extensions'); /** If you want to parse content from node_modules instead of providing a relative/absolute path, diff --git a/packages/module/src/BulkSelect/__snapshots__/BulkSelect.test.tsx.snap b/packages/module/src/BulkSelect/__snapshots__/BulkSelect.test.tsx.snap index dd4832a2..f79b636b 100644 --- a/packages/module/src/BulkSelect/__snapshots__/BulkSelect.test.tsx.snap +++ b/packages/module/src/BulkSelect/__snapshots__/BulkSelect.test.tsx.snap @@ -54,11 +54,11 @@ exports[`BulkSelect component should render 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 320 512" + viewBox="0 0 20 20" width="1em" > @@ -117,11 +117,11 @@ exports[`BulkSelect component should render 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 320 512" + viewBox="0 0 20 20" width="1em" > diff --git a/packages/module/src/ExternalLinkButton/__snapshots__/ExternalLinkButton.test.tsx.snap b/packages/module/src/ExternalLinkButton/__snapshots__/ExternalLinkButton.test.tsx.snap index 44ecc7b4..e174bf4e 100644 --- a/packages/module/src/ExternalLinkButton/__snapshots__/ExternalLinkButton.test.tsx.snap +++ b/packages/module/src/ExternalLinkButton/__snapshots__/ExternalLinkButton.test.tsx.snap @@ -22,7 +22,6 @@ exports[`ExternalLinkButton component should accept IconProps 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 512 512" width="1em" > (Opens in new tab) - + + + + + + @@ -56,7 +68,6 @@ exports[`ExternalLinkButton component should accept IconProps 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 512 512" width="1em" > (Opens in new tab) - + + + + + + @@ -147,12 +171,24 @@ exports[`ExternalLinkButton component should render 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 512 512" width="1em" > - + + + + + + @@ -176,12 +212,24 @@ exports[`ExternalLinkButton component should render 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 512 512" width="1em" > - + + + + + + diff --git a/packages/module/src/LogSnippet/__snapshots__/LogSnippet.test.tsx.snap b/packages/module/src/LogSnippet/__snapshots__/LogSnippet.test.tsx.snap index b377ef71..c7410c80 100644 --- a/packages/module/src/LogSnippet/__snapshots__/LogSnippet.test.tsx.snap +++ b/packages/module/src/LogSnippet/__snapshots__/LogSnippet.test.tsx.snap @@ -27,11 +27,11 @@ exports[`LogSnippet component should render LogSnippet component 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 512 512" + viewBox="0 0 32 32" width="1em" > @@ -96,11 +96,11 @@ exports[`LogSnippet component should render LogSnippet component 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 512 512" + viewBox="0 0 32 32" width="1em" > diff --git a/packages/module/src/MultiContentCard/__snapshots__/MultiContentCard.test.tsx.snap b/packages/module/src/MultiContentCard/__snapshots__/MultiContentCard.test.tsx.snap index 39596a22..de8aed77 100644 --- a/packages/module/src/MultiContentCard/__snapshots__/MultiContentCard.test.tsx.snap +++ b/packages/module/src/MultiContentCard/__snapshots__/MultiContentCard.test.tsx.snap @@ -17,7 +17,7 @@ exports[`MultiContentCard component should render basic multi-content card 1`] = >

@@ -1007,7 +1139,7 @@ exports[`MultiContentCard component should render multi-content card with a sing >

diff --git a/packages/module/src/ServiceCard/__snapshots__/ServiceCard.test.tsx.snap b/packages/module/src/ServiceCard/__snapshots__/ServiceCard.test.tsx.snap index 17f54bd7..b6f867cf 100644 --- a/packages/module/src/ServiceCard/__snapshots__/ServiceCard.test.tsx.snap +++ b/packages/module/src/ServiceCard/__snapshots__/ServiceCard.test.tsx.snap @@ -31,7 +31,7 @@ exports[`ServiceCard component should render ServiceCard component 1`] = ` >
@@ -58,12 +70,24 @@ exports[`Severity component should render correctly CriticalSeverity 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 1024 1024" width="1em" > - + + + + + +
@@ -97,12 +121,24 @@ exports[`Severity component should render correctly HighSeverity 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 1024 1024" width="1em" > - + + + + + +
@@ -136,12 +172,24 @@ exports[`Severity component should render correctly LowSeverity 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 1024 1024" width="1em" > - + + + + + +

@@ -175,12 +223,24 @@ exports[`Severity component should render correctly LowSeverity 2`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 1024 1024" width="1em" > - + + + + + +
@@ -214,12 +274,24 @@ exports[`Severity component should render correctly MediumSeverity 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 1024 1024" width="1em" > - + + + + + +

@@ -253,12 +325,24 @@ exports[`Severity component should render correctly UndefinedSeverity, default 1 fill="currentColor" height="1em" role="img" - viewBox="0 0 1024 1024" width="1em" > - + + + + + + diff --git a/packages/module/src/SkeletonTableBody/__snapshots__/SkeletonTableBody.test.tsx.snap b/packages/module/src/SkeletonTableBody/__snapshots__/SkeletonTableBody.test.tsx.snap index 6e683202..ed0b70f6 100644 --- a/packages/module/src/SkeletonTableBody/__snapshots__/SkeletonTableBody.test.tsx.snap +++ b/packages/module/src/SkeletonTableBody/__snapshots__/SkeletonTableBody.test.tsx.snap @@ -7,7 +7,7 @@ exports[`SkeletonTableBody component should render correctly 1`] = `
- + + + + + + @@ -82,12 +94,24 @@ exports[`SkeletonTableHead component should render correctly with Th element col fill="currentColor" height="1em" role="img" - viewBox="0 0 256 512" width="1em" > - + + + + + + @@ -106,7 +130,7 @@ exports[`SkeletonTableHead component should render correctly with count 1`] = `
- + + + + + + @@ -75,12 +87,24 @@ exports[`Status component should render correctly 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 576 512" width="1em" > - + + + + + + @@ -192,12 +216,24 @@ exports[`Status component should render correctly link 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 512 512" width="1em" > - + + + + + + @@ -254,12 +290,24 @@ exports[`Status component should render correctly link 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 512 512" width="1em" > - + + + + + + @@ -376,12 +424,24 @@ exports[`Status component should render correctly popover 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 512 512" width="1em" > - + + + + + + @@ -442,12 +502,24 @@ exports[`Status component should render correctly popover 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 512 512" width="1em" > - + + + + + + @@ -551,12 +623,24 @@ exports[`Status component should render correctly with description 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 576 512" width="1em" > - + + + + + + @@ -609,12 +693,24 @@ exports[`Status component should render correctly with description 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 576 512" width="1em" > - + + + + + + @@ -724,12 +820,24 @@ exports[`Status component should render iconOnly correctly 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 576 512" width="1em" > - + + + + + + @@ -759,12 +867,24 @@ exports[`Status component should render iconOnly correctly 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 576 512" width="1em" > - + + + + + + diff --git a/packages/module/src/TagCount/__snapshots__/TagCount.test.tsx.snap b/packages/module/src/TagCount/__snapshots__/TagCount.test.tsx.snap index cc5f5167..207393cf 100644 --- a/packages/module/src/TagCount/__snapshots__/TagCount.test.tsx.snap +++ b/packages/module/src/TagCount/__snapshots__/TagCount.test.tsx.snap @@ -30,12 +30,24 @@ exports[`TagCount component should render a disabled tag count with no value 1`] fill="currentColor" height="1em" role="img" - viewBox="0 0 512 512" width="1em" > - + + + + + + @@ -73,12 +85,24 @@ exports[`TagCount component should render a disabled tag count with no value 1`] fill="currentColor" height="1em" role="img" - viewBox="0 0 512 512" width="1em" > - + + + + + + @@ -172,12 +196,24 @@ exports[`TagCount component should render a tag count of 11 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 512 512" width="1em" > - + + + + + + @@ -216,12 +252,24 @@ exports[`TagCount component should render a tag count of 11 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 512 512" width="1em" > - + + + + + + diff --git a/packages/module/src/Tearsheet/Tearsheet.tsx b/packages/module/src/Tearsheet/Tearsheet.tsx new file mode 100644 index 00000000..0971e7cd --- /dev/null +++ b/packages/module/src/Tearsheet/Tearsheet.tsx @@ -0,0 +1,120 @@ +import type { ReactNode, HTMLProps, FunctionComponent, MouseEvent } from 'react'; +import { css } from '@patternfly/react-styles'; +import { createUseStyles } from 'react-jss'; +import { Modal, type ModalProps } from '@patternfly/react-core'; + +const useStyles = createUseStyles({ + tearsheet: { + width: 'calc(100% - 4rem) !important', + maxWidth: 'calc(100% - 4rem) !important', + height: 'calc(100% - 4rem) !important', + maxHeight: 'calc(100% - 4rem) !important', + insetBlockStart: 'auto !important', + alignSelf: 'end !important', + borderBottomLeftRadius: '0', + borderBottomRightRadius: '0', + '&.pf-v6-c-modal-animated': { + '--pf-v6-c-modal-animated--Transition': + 'width 300ms ease, max-width 300ms ease, height 300ms ease, max-height 300ms ease, top 300ms ease, opacity 240ms cubic-bezier(0.4, 0.14, 1, 1), transform 240ms cubic-bezier(0.4, 0.14, 1, 1), visibility 0ms linear 240ms', + }, + '&.pf-v6-c-modal-animated-open': { + '--pf-v6-c-modal-animated--Transition': + 'width 300ms ease, max-width 300ms ease, height 300ms ease, max-height 300ms ease, top 300ms ease, transform 240ms cubic-bezier(0, 0, 0.2, 1), visibility 0ms linear 0ms', + }, + '&.pf-m-stack-level-1': { + width: 'calc(100% - 2rem) !important', + maxWidth: 'calc(100% - 2rem) !important', + height: 'calc(100% - 6rem) !important', + maxHeight: 'calc(100% - 6rem) !important', + }, + '&.pf-m-stack-level-2': { + width: 'calc(100% - 0rem) !important', + maxWidth: 'calc(100% - 0rem) !important', + height: 'calc(100% - 8rem) !important', + maxHeight: 'calc(100% - 8rem) !important', + }, + '&.pf-m-stack-hidden': { + width: 'calc(100% - 4rem) !important', + maxWidth: 'calc(100% - 4rem) !important', + height: 'calc(100% - 4rem) !important', + maxHeight: 'calc(100% - 4rem) !important', + opacity: '0 !important', + pointerEvents: 'none !important', + }, + }, + tearsheetInner: { + display: 'flex', + flexDirection: 'column', + overflow: 'hidden', + flex: '1 1 auto', + minHeight: 0, + marginInlineEnd: 0, + }, +}); + +export interface TearsheetProps extends HTMLProps { + /** Content rendered inside the tearsheet. Should be TearsheetHeader, TearsheetBody, and/or TearsheetFooter. */ + children: ReactNode; + /** Additional classes added to the tearsheet. */ + className?: string; + /** Flag to show the tearsheet. */ + isOpen?: boolean; + /** Visual stack level of the tearsheet. Managed automatically by TearsheetGroup. + * When used standalone: 0 (back), 1 (middle), 2 (front). + * TearsheetGroup may also assign -1 (hidden behind the stack). */ + stackLevel?: number; + /** A callback for when the close button is clicked. This prop needs to be passed to render the close button. */ + onClose?: (event: KeyboardEvent | MouseEvent) => void; + /** A callback for when the tearsheet is closed via the escape key. */ + onEscapePress?: (event: KeyboardEvent) => void; + /** The parent container to append the tearsheet to. Defaults to document.body. */ + appendTo?: HTMLElement | (() => HTMLElement); + /** Accessible name for the tearsheet as a human readable string. */ + 'aria-label'?: string; + /** Space separated list of ID's of the elements that label the tearsheet. */ + 'aria-labelledby'?: string; + /** Space separated list of ID's of the elements that describe the tearsheet. */ + 'aria-describedby'?: string; + /** Additional props spread to the underlying PatternFly Modal. */ + modalProps?: Omit; +} + +const Tearsheet: FunctionComponent = ({ + children, + className, + isOpen = false, + stackLevel: stackLevelProp, + onClose, + onEscapePress, + appendTo, + 'aria-label': ariaLabel, + 'aria-labelledby': ariaLabelledBy, + 'aria-describedby': ariaDescribedBy, + modalProps, + ...props +}: TearsheetProps) => { + const classes = useStyles(); + const stackLevel = stackLevelProp ?? 0; + const stackLevelClassname = stackLevel < 0 ? 'pf-m-stack-hidden' : `pf-m-stack-level-${stackLevel}`; + + return ( + +
+ {children} +
+
+ ); +}; +Tearsheet.displayName = 'Tearsheet'; + +export default Tearsheet; diff --git a/packages/module/src/Tearsheet/index.ts b/packages/module/src/Tearsheet/index.ts new file mode 100644 index 00000000..4342d637 --- /dev/null +++ b/packages/module/src/Tearsheet/index.ts @@ -0,0 +1,2 @@ +export { default } from './Tearsheet'; +export * from './Tearsheet'; diff --git a/packages/module/src/TearsheetBody/TearsheetBody.tsx b/packages/module/src/TearsheetBody/TearsheetBody.tsx new file mode 100644 index 00000000..3874f518 --- /dev/null +++ b/packages/module/src/TearsheetBody/TearsheetBody.tsx @@ -0,0 +1,14 @@ +import type { FunctionComponent, ReactNode } from 'react'; +import { ModalBody, type ModalBodyProps } from '@patternfly/react-core'; + +export interface TearsheetBodyProps extends ModalBodyProps { + /** Content rendered inside the tearsheet body. */ + children?: ReactNode; + /** Additional classes applied to the tearsheet body. */ + className?: string; +} + +const TearsheetBody: FunctionComponent = ({ className, ...props }: TearsheetBodyProps) => ; +TearsheetBody.displayName = 'TearsheetBody'; + +export default TearsheetBody; \ No newline at end of file diff --git a/packages/module/src/TearsheetBody/index.ts b/packages/module/src/TearsheetBody/index.ts new file mode 100644 index 00000000..7229a1b8 --- /dev/null +++ b/packages/module/src/TearsheetBody/index.ts @@ -0,0 +1,2 @@ +export { default } from './TearsheetBody'; +export * from './TearsheetBody'; diff --git a/packages/module/src/TearsheetFooter/TearsheetFooter.tsx b/packages/module/src/TearsheetFooter/TearsheetFooter.tsx new file mode 100644 index 00000000..f0ecf1d5 --- /dev/null +++ b/packages/module/src/TearsheetFooter/TearsheetFooter.tsx @@ -0,0 +1,17 @@ +import type { FunctionComponent, ReactNode } from 'react'; +import { ModalFooter, type ModalFooterProps } from '@patternfly/react-core'; + +export interface TearsheetFooterProps extends ModalFooterProps { + /** Content rendered inside the tearsheet footer. */ + children?: ReactNode; + /** Additional classes applied to the tearsheet footer. */ + className?: string; +} + +export const TearsheetFooter: FunctionComponent = ({ + className, + ...props +}: TearsheetFooterProps) => ; +TearsheetFooter.displayName = 'TearsheetFooter'; + +export default TearsheetFooter; diff --git a/packages/module/src/TearsheetFooter/index.ts b/packages/module/src/TearsheetFooter/index.ts new file mode 100644 index 00000000..97946ba7 --- /dev/null +++ b/packages/module/src/TearsheetFooter/index.ts @@ -0,0 +1,2 @@ +export { default } from './TearsheetFooter'; +export * from './TearsheetFooter'; diff --git a/packages/module/src/TearsheetGroup/TearsheetGroup.tsx b/packages/module/src/TearsheetGroup/TearsheetGroup.tsx new file mode 100644 index 00000000..4a5dcc51 --- /dev/null +++ b/packages/module/src/TearsheetGroup/TearsheetGroup.tsx @@ -0,0 +1,81 @@ +import { Children, cloneElement, isValidElement, useId, useRef, type FunctionComponent, type ReactElement } from 'react'; +import Tearsheet, { type TearsheetProps } from '../Tearsheet'; + +const MAX_VISIBLE_LEVELS = 3; + +export interface TearsheetGroupProps { + /** Set of Tearsheets to render inside the group. Render order determines stacking + * priority — later children stack in front of earlier ones. Only Tearsheets with + * isOpen={true} participate in the visual stack. */ + children?: React.ReactNode; + /** Additional classes added to the Tearsheet group. */ + className?: string; + /** Unique id for the Tearsheet group. A random id is generated when not provided. */ + id?: string; +} + +const TearsheetGroup: FunctionComponent = ({ + children, + className, + id, + ...props +}: TearsheetGroupProps) => { + const generatedId = useId(); + const groupId = id ?? generatedId; + // Track each child's last assigned stack level so closing tearsheets keep + // their position during the modal exit animation instead of snapping to L0. + const prevLevelsRef = useRef>(new Map()); + + const openIndices: number[] = []; + Children.forEach(children, (child, index) => { + if (isValidElement(child) && child.type === Tearsheet && (child.props as TearsheetProps).isOpen) { + openIndices.push(index); + } + }); + + const totalOpen = openIndices.length; + const hiddenThreshold = Math.max(0, totalOpen - MAX_VISIBLE_LEVELS); + + const enhancedChildren = Children.map(children, (child, index) => { + if (!isValidElement(child) || child.type !== Tearsheet) { + return child; + } + + const openPosition = openIndices.indexOf(index); + + if (openPosition === -1) { + // Use the last known level if this tearsheet was previously open, so the + // modal close animation plays without a conflicting size/position shift. + // For never-opened tearsheets, prime them at the level they'd occupy if + // they opened next (the frontmost slot). The tearsheet is invisible at + // this point, so the pre-sizing has no visual effect — but it prevents a + // width/height/top transition from firing alongside the modal enter + // animation when the tearsheet does open. + const level = prevLevelsRef.current.get(index) ?? Math.min(totalOpen, MAX_VISIBLE_LEVELS - 1); + return cloneElement(child as ReactElement, { + stackLevel: level + }); + } + + // Levels fill from 0 upward: 1 open → L0, 2 open → L0+L1, 3 open → L0+L1+L2. + // Once all 3 visible slots are used, earlier tearsheets hide behind the stack. + const stackLevel = openPosition < hiddenThreshold ? -1 : openPosition - hiddenThreshold; + const isFrontmost = openPosition === totalOpen - 1; + + prevLevelsRef.current.set(index, stackLevel); + + return cloneElement(child as ReactElement, { + stackLevel, + modalProps: { disableFocusTrap: !isFrontmost } + }); + }); + + return ( +
+ {enhancedChildren} +
+ ); +}; +TearsheetGroup.displayName = 'TearsheetGroup'; + +export default TearsheetGroup; diff --git a/packages/module/src/TearsheetGroup/index.ts b/packages/module/src/TearsheetGroup/index.ts new file mode 100644 index 00000000..a6827ad9 --- /dev/null +++ b/packages/module/src/TearsheetGroup/index.ts @@ -0,0 +1,2 @@ +export { default } from './TearsheetGroup'; +export * from './TearsheetGroup'; diff --git a/packages/module/src/TearsheetHeader/TearsheetHeader.tsx b/packages/module/src/TearsheetHeader/TearsheetHeader.tsx new file mode 100644 index 00000000..932e6e6a --- /dev/null +++ b/packages/module/src/TearsheetHeader/TearsheetHeader.tsx @@ -0,0 +1,17 @@ +import type { FunctionComponent, ReactNode } from 'react'; +import { ModalHeader, type ModalHeaderProps } from '@patternfly/react-core'; + +export interface TearsheetHeaderProps extends ModalHeaderProps { + /** Content rendered inside the tearsheet header. */ + children?: ReactNode; + /** Additional classes applied to the tearsheet header. */ + className?: string; +} + +const TearsheetHeader: FunctionComponent = ({ + className, + ...props +}: TearsheetHeaderProps) => ; +TearsheetHeader.displayName = 'TearsheetHeader'; + +export default TearsheetHeader; diff --git a/packages/module/src/TearsheetHeader/index.ts b/packages/module/src/TearsheetHeader/index.ts new file mode 100644 index 00000000..eb954f6d --- /dev/null +++ b/packages/module/src/TearsheetHeader/index.ts @@ -0,0 +1,2 @@ +export { default } from './TearsheetHeader'; +export * from './TearsheetHeader'; diff --git a/packages/module/src/UnauthorizedAccess/__snapshots__/UnauthorizedAccess.test.tsx.snap b/packages/module/src/UnauthorizedAccess/__snapshots__/UnauthorizedAccess.test.tsx.snap index f246db42..42f56d44 100644 --- a/packages/module/src/UnauthorizedAccess/__snapshots__/UnauthorizedAccess.test.tsx.snap +++ b/packages/module/src/UnauthorizedAccess/__snapshots__/UnauthorizedAccess.test.tsx.snap @@ -20,12 +20,24 @@ exports[`UnauthorizedAccess component should apply custom styles 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 448 512" width="1em" > - + + + + + +
- + + + + + +
- + + + + + +
- + + + + + +
- + + + + + +
- + + + + + +
- + + + + + +
- + + + + + +
- + + + + + +
- + + + + + +
- + + + + + +
@@ -73,11 +73,11 @@ exports[`WarningModal component should render 1`] = ` fill="currentColor" height="1em" role="img" - viewBox="0 0 576 512" + viewBox="0 0 32 32" width="1em" > diff --git a/packages/module/src/index.ts b/packages/module/src/index.ts index 4f23c2d4..c1ff0b47 100644 --- a/packages/module/src/index.ts +++ b/packages/module/src/index.ts @@ -9,6 +9,21 @@ export * from './UnavailableContent'; export { default as UnauthorizedAccess } from './UnauthorizedAccess'; export * from './UnauthorizedAccess'; +export { default as TearsheetHeader } from './TearsheetHeader'; +export * from './TearsheetHeader'; + +export { default as TearsheetGroup } from './TearsheetGroup'; +export * from './TearsheetGroup'; + +export { default as TearsheetFooter } from './TearsheetFooter'; +export * from './TearsheetFooter'; + +export { default as TearsheetBody } from './TearsheetBody'; +export * from './TearsheetBody'; + +export { default as Tearsheet } from './Tearsheet'; +export * from './Tearsheet'; + export { default as TagCount } from './TagCount'; export * from './TagCount'; diff --git a/packages/module/utils.js b/packages/module/utils.js new file mode 100644 index 00000000..e88e4ec2 --- /dev/null +++ b/packages/module/utils.js @@ -0,0 +1,36 @@ +/** + * Cross-platform path utilities for build scripts. + * + * On Windows, Node's path.resolve and path.relative produce backslash-separated + * paths. glob v10+ treats backslashes as escape characters (not path separators), + * so a pattern like `C:\src\*\index.ts` silently matches nothing because \* is + * interpreted as a literal asterisk. glob also returns backslash paths on Windows, + * breaking downstream code that splits on '/' to extract path segments. + * + * These utilities normalize paths to POSIX forward slashes for glob input/output + * and for any path strings written into generated package.json files. + */ + +const { globSync } = require('glob'); +const path = require('path'); + +/** Normalize a file path to use POSIX forward slashes. No-op on Linux/macOS. */ +const toPosixPath = (filePath) => filePath.replace(/\\/g, '/'); + +/** + * globSync wrapper that normalizes the pattern and results to POSIX paths. + * Use in place of globSync wherever the pattern includes Node-resolved paths + * (process.cwd(), path.resolve, __dirname, etc.) that may contain backslashes. + */ +const posixGlobSync = (pattern) => + globSync(toPosixPath(pattern)).map(toPosixPath); + +/** + * path.relative wrapper that returns a POSIX-style relative path. + * Use when the result will be written into a generated file (e.g. package.json + * "main"/"module" fields) where forward slashes are expected by consumers. + */ +const posixRelative = (from, to) => + toPosixPath(path.relative(from, to)); + +module.exports = { toPosixPath, posixGlobSync, posixRelative }; diff --git a/scripts/parse-dynamic-modules.mjs b/scripts/parse-dynamic-modules.mjs index 6a35cec7..97c88ba1 100644 --- a/scripts/parse-dynamic-modules.mjs +++ b/scripts/parse-dynamic-modules.mjs @@ -76,8 +76,10 @@ const getDynamicModuleMap = ( return {}; } + const normalizedBasePath = basePath.replace(/\\/g, '/'); + /** @type {Record} */ - const dynamicModulePathToPkgDir = glob.sync(`${basePath}/dist/dynamic/**/package.json`).reduce((acc, pkgFile) => { + const dynamicModulePathToPkgDir = glob.sync(`${normalizedBasePath}/dist/dynamic/**/package.json`).reduce((acc, pkgFile) => { const pkg = require(pkgFile); const pkgModule = pkg[resolutionField]; @@ -86,7 +88,7 @@ const getDynamicModuleMap = ( } const pkgResolvedPath = path.resolve(path.dirname(pkgFile), pkgModule); - const pkgRelativePath = path.dirname(path.relative(basePath, pkgFile)); + const pkgRelativePath = path.dirname(path.relative(basePath, pkgFile)).replace(/\\/g, '/'); acc[pkgResolvedPath] = pkgRelativePath;