diff --git a/packages/module/package.json b/packages/module/package.json index 75843d6..ee2601a 100644 --- a/packages/module/package.json +++ b/packages/module/package.json @@ -9,7 +9,7 @@ "tag": "alpha" }, "scripts": { - "build": "yarn build:esm && yarn build:cjs", + "build": "yarn generate && yarn build:esm && yarn build:cjs", "build:watch": "npm run build:esm -- --watch", "build:esm": "tsc --build --verbose ./tsconfig.json", "build:cjs": "tsc --build --verbose ./tsconfig.cjs.json", @@ -18,6 +18,7 @@ "docs:build": "pf-docs-framework build all --output public", "docs:serve": "pf-docs-framework serve public --port 5001", "docs:screenshots": "pf-docs-framework screenshots --urlPrefix http://localhost:5000", + "generate": "yarn clean && node scripts/writeClassMaps.js", "test:a11y": "patternfly-a11y --config patternfly-a11y.config", "serve:a11y": "yarn serve coverage" }, @@ -47,6 +48,7 @@ "resize-observer-polyfill": "^1.5.1", "tslib": "^2.0.0", "react-monaco-editor": "^0.51.0", - "monaco-editor": "^0.34.1" + "monaco-editor": "^0.34.1", + "camel-case": "^3.0.0" } } diff --git a/packages/module/scripts/generateClassMaps.js b/packages/module/scripts/generateClassMaps.js new file mode 100644 index 0000000..775d2f3 --- /dev/null +++ b/packages/module/scripts/generateClassMaps.js @@ -0,0 +1,73 @@ +const path = require('path'); +const fs = require('fs-extra'); +const glob = require('glob'); +const camelcase = require('camel-case'); + +/** + * @param {string} cssString - CSS string + */ +function getCSSClasses(cssString) { + return cssString.match(/(\.)(?!\d)([^\s.,{[>+~#:)]*)(?![^{]*})/g); +} + +/** + * @param {string} className - Class name + */ +function formatClassName(className) { + return camelcase(className.replace(/pf-(v6-)?((c|l|m|u|is|has)-)?/g, '')); +} + +/** + * @param {string} className - Class name + */ +function isModifier(className) { + return Boolean(className && className.startsWith) && className.startsWith('.pf-m-'); +} + +/** + * @param {string} cssString - CSS string + */ +function getClassMaps(cssString) { + const res = {}; + const distinctClasses = new Set(getCSSClasses(cssString)); + + distinctClasses.forEach((className) => { + const key = formatClassName(className); + const value = className.replace('.', '').trim(); + if (isModifier(className)) { + res.modifiers = res.modifiers || {}; + res.modifiers[key] = value; + } else { + res[key] = value; + } + }); + + const ordered = {}; + Object.keys(res) + .sort() + .forEach((key) => (ordered[key] = res[key])); + + return ordered; +} + +/** + * @returns {any} Map of file names to classMaps + */ +function generateClassMaps() { + const cssFiles = glob.sync('src/**/*.css', { + absolute: true + }); + + const res = {}; + cssFiles + .map((file) => path.resolve(file)) // Normalize path for Windows + .forEach((file) => { + res[file] = getClassMaps(fs.readFileSync(file, 'utf8')); + }); + + return res; +} + +module.exports = { + generateClassMaps +}; diff --git a/packages/module/scripts/writeClassMaps.js b/packages/module/scripts/writeClassMaps.js new file mode 100644 index 0000000..d4fbf83 --- /dev/null +++ b/packages/module/scripts/writeClassMaps.js @@ -0,0 +1,47 @@ +const { join, basename, relative, dirname } = require('path'); +const { outputFileSync, copyFileSync, ensureDirSync, symlinkSync } = require('fs-extra'); +const { generateClassMaps } = require('./generateClassMaps'); + +const writeTsExport = (file, classMap, outDir) => + outputFileSync( + join(outDir, file.replace(/.css$/, '.ts')), + ` +import './${basename(file, '.css.js')}'; +export default ${JSON.stringify(classMap, null, 2)}; +`.trim() + ); + +/** + * @param {any} classMaps Map of file names to classMaps + */ +function writeClassMaps(classMaps) { + Object.entries(classMaps).forEach(([file, classMap]) => { + const packageBase = dirname(require.resolve('@patternfly/react-log-viewer/package.json')); + const relativeFilePath = relative(packageBase, file); + + // write the export map in TS and put it in src, from here TS will compile it to the different module types at build time + writeTsExport(relativeFilePath, classMap, packageBase); + + // copy the css file itself over to dist since TS won't do that + const cssFileName = basename(file); + const distDir = join(packageBase, 'dist'); + const cssDistDir = join(distDir, 'css'); + ensureDirSync(cssDistDir); + copyFileSync(file, join(cssDistDir, cssFileName)); + + // create symlinks for each exported module that reference to the single copy of the css files, prevents needing duplicates of the stylesheets + const fileDir = dirname(relativeFilePath).replace('src/', ''); + const cssDistEsmDir = join(distDir, 'esm', fileDir); + const cssDistCjsDir = join(distDir, 'js', fileDir); + const cssDistDirs = [cssDistEsmDir, cssDistCjsDir]; + cssDistDirs.forEach((dir) => { + ensureDirSync(dir); + symlinkSync(join(cssDistDir, cssFileName), join(dir, cssFileName)); + }); + }); + + // eslint-disable-next-line no-console + console.log('Wrote', Object.keys(classMaps).length * 3, 'CSS-in-JS files'); +} + +writeClassMaps(generateClassMaps()); diff --git a/packages/module/src/LogViewer/LogViewer.tsx b/packages/module/src/LogViewer/LogViewer.tsx index 4437f13..62c3a82 100644 --- a/packages/module/src/LogViewer/LogViewer.tsx +++ b/packages/module/src/LogViewer/LogViewer.tsx @@ -4,7 +4,7 @@ import { css } from '@patternfly/react-styles'; import { LogViewerRow } from './LogViewerRow'; import { parseConsoleOutput, searchedKeyWordType, stripAnsi } from './utils/utils'; import { VariableSizeList as List, areEqual } from '../react-window'; -import styles from '@patternfly/react-styles/css/components/LogViewer/log-viewer'; +import styles from './css/log-viewer'; import AnsiUp from '../ansi_up/ansi_up'; interface LogViewerProps { @@ -312,7 +312,7 @@ const LogViewerBase: React.FunctionComponent = memo( hasLineNumbers && styles.modifiers.lineNumbers, !isTextWrapped && styles.modifiers.nowrap, initialIndexWidth && styles.modifiers.lineNumberChars, - theme === 'dark' && styles.modifiers.dark + theme === 'dark' && styles.themeDark )} {...(initialIndexWidth && { style: { diff --git a/packages/module/src/LogViewer/LogViewerRow.tsx b/packages/module/src/LogViewer/LogViewerRow.tsx index 01be88f..41d2f96 100644 --- a/packages/module/src/LogViewer/LogViewerRow.tsx +++ b/packages/module/src/LogViewer/LogViewerRow.tsx @@ -1,7 +1,7 @@ import React, { memo, useContext } from 'react'; import { LOGGER_LINE_NUMBER_INDEX_DELTA } from './utils/constants'; import { css } from '@patternfly/react-styles'; -import styles from '@patternfly/react-styles/css/components/LogViewer/log-viewer'; +import styles from './css/log-viewer'; import { LogViewerContext } from './LogViewerContext'; import AnsiUp from '../ansi_up/ansi_up'; import { escapeString, escapeTextForHtml, isAnsi, searchedKeyWordType, splitAnsi } from './utils/utils'; diff --git a/packages/module/src/LogViewer/css/log-viewer.css b/packages/module/src/LogViewer/css/log-viewer.css new file mode 100644 index 0000000..955ec38 --- /dev/null +++ b/packages/module/src/LogViewer/css/log-viewer.css @@ -0,0 +1,173 @@ +.pf-v6-c-log-viewer { + --pf-v6-c-log-viewer--Height: 100%; + --pf-v6-c-log-viewer--MaxHeight: auto; + --pf-v6-c-log-viewer--m-line-numbers__index--Display: inline; + --pf-v6-c-log-viewer__header--MarginBlockEnd: var(--pf-t--global--spacer--sm); + --pf-v6-c-log-viewer__main--BackgroundColor: var(--pf-t--global--background--color--primary--default); + --pf-v6-c-log-viewer__main--BorderWidth: var(--pf-t--global--border--width--box--default); + --pf-v6-c-log-viewer__main--BorderColor: var(--pf-t--global--border--color--default); + --pf-v6-c-log-viewer__scroll-container--Height: 37.5rem; + --pf-v6-c-log-viewer__scroll-container--PaddingBlockStart: var(--pf-t--global--spacer--sm); + --pf-v6-c-log-viewer__scroll-container--PaddingBlockEnd: var(--pf-t--global--spacer--sm); + --pf-v6-c-log-viewer--m-line-numbers__list--before--InsetBlockStart: 0; + --pf-v6-c-log-viewer--m-line-numbers__list--before--InsetBlockEnd: 0; + --pf-v6-c-log-viewer--m-line-numbers__list--before--Width: var(--pf-t--global--border--width--divider--default); + --pf-v6-c-log-viewer--m-line-numbers__list--before--BackgroundColor: var(--pf-t--global--border--color--default); + --pf-v6-c-log-viewer__list--Height: auto; + --pf-v6-c-log-viewer--m-line-numbers__list--InsetInlineStart: var(--pf-v6-c-log-viewer__index--Width); + --pf-v6-c-log-viewer__list--FontFamily: var(--pf-t--global--font--family--mono); + --pf-v6-c-log-viewer__list--FontSize: var(--pf-t--global--font--size--body--sm); + --pf-v6-c-log-viewer__index--Display: none; + --pf-v6-c-log-viewer__index--Width: 4.0625rem; + --pf-v6-c-log-viewer__index--PaddingInlineEnd: var(--pf-t--global--spacer--xl); + --pf-v6-c-log-viewer__index--PaddingInlineStart: var(--pf-t--global--spacer--lg); + --pf-v6-c-log-viewer__index--Color: var(--pf-t--global--text--color--subtle); + --pf-v6-c-log-viewer__index--BackgroundColor: transparent; + --pf-v6-c-log-viewer--line-number-chars: 4.4; + --pf-v6-c-log-viewer--m-line-number-chars__index--PaddingInlineEnd: var(--pf-t--global--spacer--xs); + --pf-v6-c-log-viewer--m-line-number-chars__index--Width: calc(1ch * var(--pf-v6-c-log-viewer--line-number-chars) + var(--pf-v6-c-log-viewer__index--PaddingInlineEnd) + var(--pf-v6-c-log-viewer__index--PaddingInlineStart)); + --pf-v6-c-log-viewer__text--PaddingInlineEnd: var(--pf-t--global--spacer--md); + --pf-v6-c-log-viewer__text--PaddingInlineStart: var(--pf-t--global--spacer--md); + --pf-v6-c-log-viewer__text--Color: var(--pf-t--global--text--color--regular); + --pf-v6-c-log-viewer__text--WordBreak: break-all; + --pf-v6-c-log-viewer__text--WhiteSpace: break-spaces; + --pf-v6-c-log-viewer__text--LineBreak: anywhere; + --pf-v6-c-log-viewer--m-nowrap__text--WhiteSpace: nowrap; + --pf-v6-c-log-viewer__string--m-match--Color: var(--pf-t--global--text--color--on-highlight); + --pf-v6-c-log-viewer__string--m-match--BackgroundColor: var(--pf-t--global--background--color--highlight--default); + --pf-v6-c-log-viewer__string--m-current--Color: var(--pf-t--global--text--color--on-highlight); + --pf-v6-c-log-viewer__string--m-current--BackgroundColor: var(--pf-t--global--background--color--highlight--clicked); + --pf-v6-c-log-viewer__timestamp--FontWeight: var(--pf-t--global--font--weight--body--bold); + --pf-v6-c-log-viewer--c-toolbar--PaddingBlockStart: 0; + --pf-v6-c-log-viewer--c-toolbar--PaddingBlockEnd: 0; + --pf-v6-c-log-viewer--c-toolbar__content--PaddingInlineEnd: 0; + --pf-v6-c-log-viewer--c-toolbar__content--PaddingInlineStart: 0; + --pf-v6-c-log-viewer--c-toolbar__group--m-toggle-group--spacer: 0; + --pf-v6-c-log-viewer--c-toolbar__group--m-toggle-group--m-show--spacer: var(--pf-t--global--spacer--sm); + --pf-v6-c-log-viewer--m-dark__main--BorderWidth: 0; + display: flex; + flex-direction: column; + height: var(--pf-v6-c-log-viewer--Height); + max-height: var(--pf-v6-c-log-viewer--MaxHeight); +} +.pf-v6-c-log-viewer.pf-v6-theme-dark { + --pf-v6-c-log-viewer__main--BorderWidth: var(--pf-v6-c-log-viewer--m-dark__main--BorderWidth); +} +.pf-v6-c-log-viewer.pf-v6-theme-dark .pf-v6-c-log-viewer__main { + --pf-v6-c-log-viewer__main--BackgroundColor: var(--pf-t--global--background--color--primary--default); + --pf-v6-c-log-viewer__main--BorderColor: var(--pf-t--global--border--color--default); + --pf-v6-c-log-viewer__text--Color: var(--pf-t--global--text--color--regular); + --pf-v6-c-log-viewer__index--Color: var(--pf-t--global--text--color--subtle); + --pf-v6-c-log-viewer--m-line-numbers__list--before--BackgroundColor: var(--pf-t--global--border--color--default); +} +.pf-v6-c-log-viewer.pf-m-wrap-text { + word-break: break-all; +} +.pf-v6-c-log-viewer.pf-m-nowrap { + --pf-v6-c-log-viewer__text--WhiteSpace: var(--pf-v6-c-log-viewer--m-nowrap__text--WhiteSpace); +} +.pf-v6-c-log-viewer.pf-m-line-numbers { + --pf-v6-c-log-viewer__index--Display: var(--pf-v6-c-log-viewer--m-line-numbers__index--Display); +} +.pf-v6-c-log-viewer.pf-m-line-numbers .pf-v6-c-log-viewer__list { + position: absolute; + inset-inline-start: var(--pf-v6-c-log-viewer--m-line-numbers__list--InsetInlineStart); + inset-inline-end: 0; +} +.pf-v6-c-log-viewer.pf-m-line-numbers .pf-v6-c-log-viewer__list::before { + position: absolute; + inset-block-start: var(--pf-v6-c-log-viewer--m-line-numbers__list--before--InsetBlockStart); + inset-block-end: var(--pf-v6-c-log-viewer--m-line-numbers__list--before--InsetBlockEnd); + inset-inline-start: 0; + width: var(--pf-v6-c-log-viewer--m-line-numbers__list--before--Width); + content: ""; + background: var(--pf-v6-c-log-viewer--m-line-numbers__list--before--BackgroundColor); +} +.pf-v6-c-log-viewer.pf-m-line-number-chars { + --pf-v6-c-log-viewer__index--PaddingInlineEnd: var(--pf-v6-c-log-viewer--m-line-number-chars__index--PaddingInlineEnd); + --pf-v6-c-log-viewer__index--Width: var(--pf-v6-c-log-viewer--m-line-number-chars__index--Width); +} +.pf-v6-c-log-viewer .pf-v6-c-toolbar { + --pf-v6-c-toolbar--PaddingBlockStart: var(--pf-v6-c-log-viewer--c-toolbar--PaddingBlockStart); + --pf-v6-c-toolbar--PaddingBlockEnd: var(--pf-v6-c-log-viewer--c-toolbar--PaddingBlockEnd); + --pf-v6-c-toolbar__content--PaddingInlineEnd: var(--pf-v6-c-log-viewer--c-toolbar__content--PaddingInlineEnd); + --pf-v6-c-toolbar__content--PaddingInlineStart: var(--pf-v6-c-log-viewer--c-toolbar__content--PaddingInlineStart); + --pf-v6-c-toolbar__group--m-toggle-group--spacer: 0; + --pf-v6-c-toolbar__group--m-toggle-group--m-show--spacer: var(--pf-v6-c-log-viewer--c-toolbar__group--m-toggle-group--m-show--spacer); +} +.pf-v6-c-log-viewer .pf-v6-c-toolbar__content-section { + flex-wrap: nowrap; +} + +.pf-v6-c-log-viewer__header { + margin-block-end: var(--pf-v6-c-log-viewer__header--MarginBlockEnd); +} + +.pf-v6-c-log-viewer__main { + display: flex; + flex-direction: column; + min-height: 0; + background-color: var(--pf-v6-c-log-viewer__main--BackgroundColor); + border: var(--pf-v6-c-log-viewer__main--BorderWidth) solid var(--pf-v6-c-log-viewer__main--BorderColor); +} + +.pf-v6-c-log-viewer__scroll-container { + position: relative; + height: var(--pf-v6-c-log-viewer__scroll-container--Height); + padding-block-start: var(--pf-v6-c-log-viewer__scroll-container--PaddingBlockStart); + padding-block-end: var(--pf-v6-c-log-viewer__scroll-container--PaddingBlockEnd); + overflow: auto; + will-change: transform; + direction: ltr; +} + +.pf-v6-c-log-viewer__list { + position: relative; + height: var(--pf-v6-c-log-viewer__list--Height); + font-family: var(--pf-v6-c-log-viewer__list--FontFamily); + font-size: var(--pf-v6-c-log-viewer__list--FontSize); +} + +.pf-v6-c-log-viewer__list-item { + inset-inline-start: 0; + width: 100%; +} + +.pf-v6-c-log-viewer__string.pf-m-match { + color: var(--pf-v6-c-log-viewer__string--m-match--Color, inherit); + background-color: var(--pf-v6-c-log-viewer__string--m-match--BackgroundColor); +} +.pf-v6-c-log-viewer__string.pf-m-current { + color: var(--pf-v6-c-log-viewer__string--m-current--Color, inherit); + background-color: var(--pf-v6-c-log-viewer__string--m-current--BackgroundColor); +} + +.pf-v6-c-log-viewer__index { + position: fixed; + inset-inline-start: 0; + display: var(--pf-v6-c-log-viewer__index--Display); + width: var(--pf-v6-c-log-viewer__index--Width); + padding-inline-start: var(--pf-v6-c-log-viewer__index--PaddingInlineStart); + padding-inline-end: var(--pf-v6-c-log-viewer__index--PaddingInlineEnd); + font-family: var(--pf-v6-c-log-viewer__index--FontFamily, inherit); + font-size: var(--pf-v6-c-log-viewer__index--FontSize, inherit); + color: var(--pf-v6-c-log-viewer__index--Color); + user-select: none; + background-color: var(--pf-v6-c-log-viewer__index--BackgroundColor); +} + +.pf-v6-c-log-viewer__text { + display: block; + padding-inline-start: var(--pf-v6-c-log-viewer__text--PaddingInlineStart); + padding-inline-end: var(--pf-v6-c-log-viewer__text--PaddingInlineEnd); + font-family: var(--pf-v6-c-log-viewer__text--FontFamily, inherit); + font-size: var(--pf-v6-c-log-viewer__text--FontSize, inherit); + color: var(--pf-v6-c-log-viewer__text--Color); + word-break: var(--pf-v6-c-log-viewer__text--WordBreak); + white-space: var(--pf-v6-c-log-viewer__text--WhiteSpace); + line-break: var(--pf-v6-c-log-viewer__text--LineBreak); +} + +.pf-v6-c-log-viewer__timestamp { + font-weight: var(--pf-v6-c-log-viewer__timestamp--FontWeight); +} \ No newline at end of file diff --git a/packages/module/src/LogViewer/css/log-viewer.ts b/packages/module/src/LogViewer/css/log-viewer.ts new file mode 100644 index 0000000..43565ec --- /dev/null +++ b/packages/module/src/LogViewer/css/log-viewer.ts @@ -0,0 +1,24 @@ +import './log-viewer.css'; +export default { + "logViewer": "pf-v6-c-log-viewer", + "logViewerHeader": "pf-v6-c-log-viewer__header", + "logViewerIndex": "pf-v6-c-log-viewer__index", + "logViewerList": "pf-v6-c-log-viewer__list", + "logViewerListItem": "pf-v6-c-log-viewer__list-item", + "logViewerMain": "pf-v6-c-log-viewer__main", + "logViewerScrollContainer": "pf-v6-c-log-viewer__scroll-container", + "logViewerString": "pf-v6-c-log-viewer__string", + "logViewerText": "pf-v6-c-log-viewer__text", + "logViewerTimestamp": "pf-v6-c-log-viewer__timestamp", + "modifiers": { + "wrapText": "pf-m-wrap-text", + "nowrap": "pf-m-nowrap", + "lineNumbers": "pf-m-line-numbers", + "lineNumberChars": "pf-m-line-number-chars", + "match": "pf-m-match", + "current": "pf-m-current" + }, + "themeDark": "pf-v6-theme-dark", + "toolbar": "pf-v6-c-toolbar", + "toolbarContentSection": "pf-v6-c-toolbar__content-section" +}; \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 2ca247f..2509fda 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4067,6 +4067,14 @@ callsites@^3.0.0: resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== +camel-case@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-3.0.0.tgz#ca3c3688a4e9cf3a4cda777dc4dcbc713249cf73" + integrity sha512-+MbKztAYHXPr1jNTSKQF52VpcFjwY5RkR7fxksV8Doo4KAYc5Fl4UJRgthBbTmEx8C54DqahhbLJkDwjI3PI/w== + dependencies: + no-case "^2.2.0" + upper-case "^1.1.1" + camel-case@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-4.1.2.tgz#9728072a954f805228225a6deea6b38461e1bd5a" @@ -7952,6 +7960,11 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: dependencies: js-tokens "^3.0.0 || ^4.0.0" +lower-case@^1.1.1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac" + integrity sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA== + lower-case@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz" @@ -8389,6 +8402,13 @@ neo-async@^2.6.0, neo-async@^2.6.1, neo-async@^2.6.2: resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== +no-case@^2.2.0: + version "2.3.2" + resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac" + integrity sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ== + dependencies: + lower-case "^1.1.1" + no-case@^3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz" @@ -11384,6 +11404,11 @@ update-notifier@^2.2.0: semver-diff "^2.0.0" xdg-basedir "^3.0.0" +upper-case@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-1.1.3.tgz#f6b4501c2ec4cdd26ba78be7222961de77621598" + integrity sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA== + uri-js@^4.2.2: version "4.4.1" resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz"