diff --git a/package.json b/package.json index 8a00c85d12b..ecae4943afe 100644 --- a/package.json +++ b/package.json @@ -120,5 +120,10 @@ "packages/*", "packages/react-integration/demo-app-ts" ] + }, + "dependencies": { + "@patternfly/react-component-groups": "^6.2.1", + "clsx": "^2.1.1", + "react-jss": "^10.10.0" } } diff --git a/packages/react-core/src/demos/Animations/Animations.md b/packages/react-core/src/demos/Animations/Animations.md new file mode 100644 index 00000000000..680e9aa4957 --- /dev/null +++ b/packages/react-core/src/demos/Animations/Animations.md @@ -0,0 +1,56 @@ +--- +id: Motion +section: design-foundations +source: react-demos +--- + +import { Fragment, useRef, useState, useEffect, useCallback } from 'react'; + +import BellIcon from '@patternfly/react-icons/dist/esm/icons/bell-icon'; +import CogIcon from '@patternfly/react-icons/dist/esm/icons/cog-icon'; +import BarsIcon from '@patternfly/react-icons/dist/js/icons/bars-icon'; +import HelpIcon from '@patternfly/react-icons/dist/esm/icons/help-icon'; +import PowerOffIcon from '@patternfly/react-icons/dist/esm/icons/power-off-icon'; +import QuestionCircleIcon from '@patternfly/react-icons/dist/esm/icons/question-circle-icon'; +import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon'; +import imgAvatar from '@patternfly/react-core/src/components/assets/avatarImg.svg'; +import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon'; +import pfLogo from '@patternfly/react-core/src/demos/assets/PF-HorizontalLogo-Color.svg'; +import MultiContentCard from "@patternfly/react-component-groups/dist/dynamic/MultiContentCard"; +import { ArrowRightIcon, LockIcon, PortIcon, CubeIcon, AutomationIcon, ExclamationCircleIcon, CheckCircleIcon, ExclamationTriangleIcon, HamburgerIcon, TimesIcon} from '@patternfly/react-icons'; +import { createUseStyles } from 'react-jss'; +import clsx from 'clsx'; +import UnpluggedIcon from '@patternfly/react-icons/dist/esm/icons/unplugged-icon'; +import l_gallery_GridTemplateColumns_min from '@patternfly/react-tokens/dist/esm/l_gallery_GridTemplateColumns_min'; +import {applicationsData} from './examples/ResourceTableData.jsx'; +import SkeletonTable from "@patternfly/react-component-groups/dist/dynamic/SkeletonTable"; +import t_global_text_color_subtle from '@patternfly/react-tokens/dist/esm/t_global_text_color_subtle'; +import { AnimationsOverview } from '@patternfly/react-core/dist/esm/demos/Animations/AnimationsOverview'; +import { AnimationsNotificationsDrawer } from '@patternfly/react-core/dist/esm/demos/Animations/AnimationsNotificationsDrawer'; +import { AnimationsHeaderToolbar } from '@patternfly/react-core/dist/esm/demos/Animations/AnimationsHeaderToolbar'; +import { AnimationsStartTourModal } from '@patternfly/react-core/dist/esm/demos/Animations/AnimationsStartTourModal'; +import { AnimationsEndTourModal } from '@patternfly/react-core/dist/esm/demos/Animations/AnimationsEndTourModal'; +import { AnimationsCreateDatabaseForm } from '@patternfly/react-core/dist/esm/demos/Animations/AnimationsCreateDatabaseForm'; +import { GuidedTourProvider, useGuidedTour } from '@patternfly/react-core/dist/esm/demos/Animations/GuidedTourContext'; +import BoltIcon from '@patternfly/react-icons/dist/esm/icons/bolt-icon'; +import { Table, Thead, Tbody, Tr, Th, Td, ExpandableRowContent } from '@patternfly/react-table'; +import PendingIcon from '@patternfly/react-icons/dist/esm/icons/pending-icon'; +import CheckIcon from '@patternfly/react-icons/dist/esm/icons/check-icon'; +import InfoIcon from '@patternfly/react-icons/dist/esm/icons/info-icon'; +import ResourcesFullIcon from '@patternfly/react-icons/dist/esm/icons/resources-full-icon'; +import openshiftLogo from '../assets/Summit-collage-depoying-openshift-product-icon-RH.png' +import emptyStateLogo from '../assets/Summit-collage-hybrid-cloud-dark-RH.png' + + + + +## Demos + +Explore the current state of [PatternFly component animations](https://github.com/orgs/patternfly/projects/7/views/66). + +To see how our components can now use motion to provide clear feedback and improve usability, this demo guides you through a UI that contains a variety of motion updates, including animated alerts, icons, expansion, and more. + +### Animated UI + +```js file="./examples/Animations.tsx" isFullscreen +``` diff --git a/packages/react-core/src/demos/Animations/AnimationsCreateDatabaseForm.tsx b/packages/react-core/src/demos/Animations/AnimationsCreateDatabaseForm.tsx new file mode 100644 index 00000000000..2693377abeb --- /dev/null +++ b/packages/react-core/src/demos/Animations/AnimationsCreateDatabaseForm.tsx @@ -0,0 +1,206 @@ +import { useRef, useState, FunctionComponent } from 'react'; +import { + AlertGroup, + Alert, + Button, + Form, + FormGroup, + FormHelperText, + FormAlert, + FormGroupLabelHelp, + HelperText, + HelperTextItem, + TextInput, + Popover, + ActionGroup +} from '../..'; +import { useGuidedTour } from './GuidedTourContext'; + +interface Props { + onClose: () => void; + emptyStateLogo?: any; +} + +export const AnimationsCreateDatabaseForm: FunctionComponent = ({ onClose }) => { + // State variables + const [name, setName] = useState(''); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + // Submit state variables + const [isSuccess, setIsSuccess] = useState(false); + const [actionCompleted, setActionCompleted] = useState(false); + const { renderTourStepElement } = useGuidedTour(); + + const labelHelpRef = useRef(null); + + // Re-introducing the type alias for validation status + type validationStatus = 'success' | 'warning' | 'error' | 'default'; + + // Reverting useState to infer the type as a generic string + const [isNameValid, setIsNameValid] = useState('default'); + const [isPasswordValid, setIsPasswordValid] = useState('default'); + const [isEmailValid, setIsEmailValid] = useState('default'); + + const handleNameChange = (_event: React.FormEvent, name: string) => { + setName(name); + }; + + const handleEmailChange = (_event: React.FormEvent, email: string) => { + setEmail(email); + }; + + const handlePasswordChange = (_event: React.FormEvent, password: string) => { + setPassword(password); + }; + + const validateName = (value: string) => /^[a-z0-9-]+$/.test(value) && value.length > 0; + const validatePassword = (value: string) => value.length >= 12 && /[0-9]/.test(value) && /[A-Z]/.test(value); + const validateEmail = (value: string) => value.includes('@'); + + const handleNameBlur = () => { + setIsNameValid(validateName(name) ? 'success' : 'error'); + }; + + const handlePasswordBlur = () => { + setIsPasswordValid(validatePassword(password) ? 'success' : 'error'); + }; + + const handleEmailBlur = () => { + setIsEmailValid(validateEmail(email) ? 'success' : 'error'); + }; + + const handleSubmit = () => { + const isNameCurrentValid = validateName(name); + const isPasswordCurrentValid = validatePassword(password); + const isEmailCurrentValid = validateEmail(email); + + setIsNameValid(isNameCurrentValid ? 'success' : 'error'); + setIsPasswordValid(isPasswordCurrentValid ? 'success' : 'error'); + setIsEmailValid(isEmailCurrentValid ? 'success' : 'error'); + + const allFieldsValid = isNameCurrentValid && isPasswordCurrentValid && isEmailCurrentValid; + + setActionCompleted(true); + setIsSuccess(allFieldsValid); + }; + + const onReset = () => { + setIsNameValid('default'); + setIsPasswordValid('default'); + setIsEmailValid('default'); + }; + + return renderTourStepElement( + 'validationErrors', +
+ {actionCompleted && isSuccess ? ( + + + + + + ) : null} + The name of your database} + bodyContent={ +
+

+ The name of your database is used to identify it in the system. It must be unique and cannot be + changed later. +

+
+ } + > + + + } + isRequired + fieldId="simple-form-name-01" + > + + {isNameValid === 'error' && ( + + + + Must contain only lowercase letters, numbers, and hyphens. + + + + )} +
+ + + {isEmailValid === 'error' && ( + + + + Must be a valid email address containing an @ symbol. + + + + )} + + + + {isPasswordValid === 'error' && ( + + + + Password must be at least 12 characters and include one uppercase letter and one number. + + + + )} + + + + + + +
+ ); +}; diff --git a/packages/react-core/src/demos/Animations/AnimationsEndTourModal.tsx b/packages/react-core/src/demos/Animations/AnimationsEndTourModal.tsx new file mode 100644 index 00000000000..7c3c6a5c174 --- /dev/null +++ b/packages/react-core/src/demos/Animations/AnimationsEndTourModal.tsx @@ -0,0 +1,33 @@ +import { FunctionComponent } from 'react'; +import { Button, Content, Modal, ModalBody, ModalFooter, ModalHeader, ModalVariant } from '../../index'; +import { useGuidedTour } from './GuidedTourContext'; + +export const AnimationsEndTourModal: FunctionComponent = () => { + const { onStart, onFinish } = useGuidedTour(); + + return ( + + + + You’ve reached the end of this tour. Thanks for exploring our new animations! + + To take the tour again, click Restart or refresh this page. + + + + + + + + ); +}; diff --git a/packages/react-core/src/demos/Animations/AnimationsHeaderToolbar.tsx b/packages/react-core/src/demos/Animations/AnimationsHeaderToolbar.tsx new file mode 100644 index 00000000000..2de77f2608e --- /dev/null +++ b/packages/react-core/src/demos/Animations/AnimationsHeaderToolbar.tsx @@ -0,0 +1,152 @@ +import { useRef, useState, FunctionComponent, RefObject, useEffect } from 'react'; +import { + Avatar, + Button, + ButtonVariant, + Dropdown, + DropdownItem, + DropdownList, + MenuToggle, + NotificationBadge, + Toolbar, + ToolbarItem, + ToolbarGroup, + ToolbarContent +} from '../..'; +import CogIcon from '@patternfly/react-icons/dist/esm/icons/cog-icon'; +import QuestionCircleIcon from '@patternfly/react-icons/dist/esm/icons/question-circle-icon'; +import imgAvatar from '@patternfly/react-core/src/components/assets/avatarImg.svg'; +import { NotificationType } from './types'; +import { useGuidedTour } from './GuidedTourContext'; + +interface Props { + notifications: NotificationType[]; + isDrawerExpanded: boolean; + setIsDrawerExpanded: (newVal: boolean) => void; + onStartGuidedTour: () => void; + onEndGuidedTour: () => void; +} + +export const AnimationsHeaderToolbar: FunctionComponent = ({ + notifications, + isDrawerExpanded, + setIsDrawerExpanded, + onStartGuidedTour, + onEndGuidedTour +}) => { + const [isActionsMenuOpen, setIsActionsMenuOpen] = useState(false); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const [shouldNotifyNewNotification, setShouldNotifyNewNotification] = useState(false); + const { renderTourStepElement, tourStep } = useGuidedTour(); + const previousUnreadCountRef = useRef(notifications.filter((n) => !n.isRead).length); + + const unreadNotificationCount = notifications.filter((n) => !n.isRead).length; + + useEffect(() => { + let timerId: NodeJS.Timeout; + + if (unreadNotificationCount > previousUnreadCountRef.current) { + setShouldNotifyNewNotification(true); + previousUnreadCountRef.current = unreadNotificationCount; + timerId = setTimeout(() => setShouldNotifyNewNotification(false), 1200); + } + return () => { + if (timerId) { + clearTimeout(timerId); + } + }; + }, [unreadNotificationCount]); + + return ( + + + + + + {renderTourStepElement( + 'notificationBadge', + setIsDrawerExpanded(!isDrawerExpanded)} + aria-label="Notifications" + isExpanded={isDrawerExpanded} + count={unreadNotificationCount} + shouldNotify={shouldNotifyNewNotification} + /> + )} + + + + {renderTourStepElement( + 'settingsButton', + + + + + )} + + + ); +}; diff --git a/packages/react-core/src/demos/Animations/AnimationsOverview.tsx b/packages/react-core/src/demos/Animations/AnimationsOverview.tsx new file mode 100644 index 00000000000..fab5f6903c9 --- /dev/null +++ b/packages/react-core/src/demos/Animations/AnimationsOverview.tsx @@ -0,0 +1,179 @@ +import { Fragment, FunctionComponent, useState } from 'react'; +import { + Button, + Content, + Card, + CardHeader, + CardBody, + CardFooter, + CardTitle, + ContentVariants, + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + Divider, + Grid, + GridItem, + PageSection, + Title +} from '../..'; +import ArrowRightIcon from '@patternfly/react-icons/dist/esm/icons/arrow-right-icon'; +import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon'; +import MultiContentCard from '@patternfly/react-component-groups/dist/dynamic/MultiContentCard'; +import AnimationsOverviewClusterInventory from './AnimationsOverviewClusterInventory'; +import AnimationsOverviewNetworkActivity from './AnimationsOverviewNetworkActivity'; +import AnimationsOverviewStorage from './AnimationsOverviewStorage'; +import AnimationsOverviewMemoryUtilization from './AnimationsOverviewMemoryUtilization'; + +interface AnimationsOverviewProps { + recentActivityCard?: React.ReactNode; + openshiftLogo?: any; +} + +export const AnimationsOverview: FunctionComponent = ({ + recentActivityCard, + openshiftLogo +}) => { + const [displayMultiContentCard, setDisplayMultiContentCard] = useState(true); + + const handleCloseMultiContentCard = () => { + setDisplayMultiContentCard(false); + }; + + const cards = [ + // Card 1: Performance + + + Animations + + + + Animations are a new way to interact with your data. They are a way to visualize your data in a way that is + easy to understand and use. + + + + + + , + // Card 2: Stability + + + Network security + + + + Network security is a critical part of any organization's security posture. + + + + + + , + // Card 3: Availability + + + Cluster alerting + + + + Cluster alerting is a critical part of any organization's security posture. + + + + + + , + // Card 4: Image + + + OpenShift Logo + + + ]; + + return ( + + {displayMultiContentCard && ( + + } variant="plain" onClick={handleCloseMultiContentCard} />} + /> + + )} + + + + + + + Cluster Details + + + + + + Cluster API Address + + https://api1.devcluster.openshift.com + + + + Cluster ID + 63b97ac1-b850-41d9-8820-239becde9e86 + + + Provide + AWS + + + OpenShift Version + 4.5.0.ci-2020-06-16-015028 + + + Update Channel + stable-4.5 + + + + + + View Settings + + + + + + + + + + + + + + + + {recentActivityCard && ( + + {recentActivityCard} + + )} + + + + ); +}; + +export default AnimationsOverview; diff --git a/packages/react-core/src/demos/Animations/AnimationsOverviewCardStatus.tsx b/packages/react-core/src/demos/Animations/AnimationsOverviewCardStatus.tsx new file mode 100644 index 00000000000..c42d215f91b --- /dev/null +++ b/packages/react-core/src/demos/Animations/AnimationsOverviewCardStatus.tsx @@ -0,0 +1,82 @@ +import { FunctionComponent } from 'react'; +import { Card, CardHeader, CardBody, Flex, FlexItem, Icon, Title, Grid, GridItem } from '../..'; +import ExclamationCircleIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon'; +import CheckCircleIcon from '@patternfly/react-icons/dist/esm/icons/check-circle-icon'; + +const AnimationsOverviewCardStatus: FunctionComponent = () => ( + + { + + + Status + + + } + + + + + + + + + + + Cluster + + + + + + + + + + + + e.preventDefault()}> + Control Panel + + + + + + + + + + + + + + Operators + + + 1 degraded + + + + + + + + + + + + + + Image Vulnerabilities + + + 0 vulnerabilities + + + + + + + +); + +export default AnimationsOverviewCardStatus; diff --git a/packages/react-core/src/demos/Animations/AnimationsOverviewClusterInventory.tsx b/packages/react-core/src/demos/Animations/AnimationsOverviewClusterInventory.tsx new file mode 100644 index 00000000000..c016397a3d3 --- /dev/null +++ b/packages/react-core/src/demos/Animations/AnimationsOverviewClusterInventory.tsx @@ -0,0 +1,110 @@ +import { useState } from 'react'; +import { + Card, + CardHeader, + CardTitle, + CardBody, + CardFooter, + Dropdown, + DropdownList, + DropdownItem, + MenuToggle, + Flex, + FlexItem, + Button, + Icon +} from '../..'; + +import ListIcon from '@patternfly/react-icons/dist/esm/icons/list-icon'; +import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon'; +import CubesIcon from '@patternfly/react-icons/dist/esm/icons/cubes-icon'; +import CubeIcon from '@patternfly/react-icons/dist/esm/icons/cube-icon'; +import ArrowRightIcon from '@patternfly/react-icons/dist/esm/icons/arrow-right-icon'; + +export const ClusterInventoryCard: React.FunctionComponent = () => { + const [isKebabOpen, setIsKebabOpen] = useState(false); + + // Data for the list items in the card body + const inventoryItems = [ + { icon: , text: '15 Deployments' }, + { icon: , text: '15 Deployments' }, + { icon: , text: '15 Deployments' }, + { icon: , text: '15 Deployments' }, + { icon: , text: '126 pods' } + ]; + + // Items for the kebab dropdown menu + const dropdownItems = [ + Action 1, + Action 2, + Action 3 + ]; + + // The kebab menu toggle button + const kebabToggle = (toggleRef: React.Ref) => ( + setIsKebabOpen(!isKebabOpen)} + isExpanded={isKebabOpen} + aria-label="Cluster inventory card kebab menu" + > + + + ); + + return ( + // To match the dark theme in your screenshot, we wrap the card in a div with a dark background. + // The `isPlain` and `isFlat` props on the Card remove its default background and shadow so it blends in. + + setIsKebabOpen(false)} + onOpenChange={(isOpen: boolean) => setIsKebabOpen(isOpen)} + toggle={kebabToggle} + popperProps={{ position: 'right' }} + > + {dropdownItems} + + ), + hasNoOffset: false, + className: '' + }} + > + + + + Cluster inventory + + + + + {/* We use another Flex layout with column direction to stack the inventory items */} + + {inventoryItems.map((item, index) => ( + + + + {item.icon} + + {item.text} + + + ))} + + + + + + + ); +}; +ClusterInventoryCard.displayName = 'ClusterInventoryCard'; + +export default ClusterInventoryCard; diff --git a/packages/react-core/src/demos/Animations/AnimationsOverviewEventsCard.tsx b/packages/react-core/src/demos/Animations/AnimationsOverviewEventsCard.tsx new file mode 100644 index 00000000000..901eac6788b --- /dev/null +++ b/packages/react-core/src/demos/Animations/AnimationsOverviewEventsCard.tsx @@ -0,0 +1,154 @@ +import { Fragment, FunctionComponent, useState } from 'react'; +import { + Card, + CardBody, + CardFooter, + CardHeader, + CardTitle, + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + Divider, + Flex, + FlexItem, + Icon, + MenuToggle, + Select, + SelectList, + SelectOption, + Spinner, + Timestamp, + Title +} from '../..'; +import CheckCircleIcon from '@patternfly/react-icons/dist/esm/icons/check-circle-icon'; +import ExclamationCircleIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon'; + +const AnimationsOverviewEventsCard: FunctionComponent = () => { + const [isOpen, setIsOpen] = useState(false); + + const selectItems = ( + + + Success + + + Error + + + Warning + + + ); + + const toggle = (toggleRef: any) => ( + setIsOpen(!isOpen)} isExpanded={isOpen} variant="plainText"> + Status + + ); + + const headerActions = ( + + ); + + return ( + + + + + + Events + + + + + + + + + + + + + + + Readiness probe failed + + + + + Readiness probe failed: Get https://10.131.0.7:5000/healthz: dial tcp 10.131.0.7:5000: connect: + connection refused + + + + + + + + + + + + + + + Successful assignment + + + + + Successfully assigned default/example to ip-10-0-130-149.ec2.internal + + + + + + + + + + + + + Pulling image + + + + Pulling image "openshift/hello-openshift" + + + + + + + + + + + + + + Created container + + + + Created container hello-openshift + + + + + + + + + View all events + + + + ); +}; + +export default AnimationsOverviewEventsCard; diff --git a/packages/react-core/src/demos/Animations/AnimationsOverviewMemoryUtilization.tsx b/packages/react-core/src/demos/Animations/AnimationsOverviewMemoryUtilization.tsx new file mode 100644 index 00000000000..cd5a1ece8a1 --- /dev/null +++ b/packages/react-core/src/demos/Animations/AnimationsOverviewMemoryUtilization.tsx @@ -0,0 +1,139 @@ +import { useState } from 'react'; +import { + Card, + CardTitle, + CardBody, + CardFooter, + Flex, + Button, + Dropdown, + DropdownItem, + MenuToggle, + CardHeader, + DropdownList, + FlexItem +} from '../..'; +import { ChartDonutThreshold, ChartDonutUtilization } from '@patternfly/react-charts/dist/esm/victory/components'; +import CubesIcon from '@patternfly/react-icons/dist/esm/icons/cubes-icon'; +import ArrowRightIcon from '@patternfly/react-icons/dist/esm/icons/arrow-right-icon'; +import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon'; + +export const MemoryUtilizationCard: React.FunctionComponent = () => { + const [isKebabOpen, setIsKebabOpen] = useState(false); + + const dropdownItems = [ + Action 1, + Action 2, + Action 3 + ]; + + // The kebab menu toggle button + const kebabToggle = (toggleRef: React.Ref) => ( + setIsKebabOpen(!isKebabOpen)} + isExpanded={isKebabOpen} + aria-label="Cluster inventory card kebab menu" + > + + + ); + + return ( + + setIsKebabOpen(false)} + onOpenChange={(isOpen: boolean) => setIsKebabOpen(isOpen)} + toggle={kebabToggle} + popperProps={{ position: 'right' }} + > + {dropdownItems} + + ), + hasNoOffset: false, + className: '' + }} + > + + + + Memory utilization + + + + + + + (datum.x ? datum.x : null)} + padding={{ + bottom: 0, + left: 0, + right: 0, + top: 10 + }} + width={200} + > + (datum.x ? `${datum.x}: ${datum.y}%` : null)} + title="95%" + subTitle="CPU" + thresholds={[{ value: 60 }, { value: 90 }]} + /> + + + + (datum.x ? datum.x : null)} + padding={{ + bottom: 0, + left: 0, + right: 0, + top: 10 + }} + width={200} + > + (datum.x ? `${datum.x}: ${datum.y}%` : null)} + title="55%" + subTitle="GPU" + thresholds={[{ value: 60 }, { value: 90 }]} + /> + + + + + + + + + ); +}; +MemoryUtilizationCard.displayName = 'MemoryUtilizationCard'; + +export default MemoryUtilizationCard; diff --git a/packages/react-core/src/demos/Animations/AnimationsOverviewNetworkActivity.tsx b/packages/react-core/src/demos/Animations/AnimationsOverviewNetworkActivity.tsx new file mode 100644 index 00000000000..5bc33f5e304 --- /dev/null +++ b/packages/react-core/src/demos/Animations/AnimationsOverviewNetworkActivity.tsx @@ -0,0 +1,121 @@ +import { useState } from 'react'; +import { Card, CardHeader, CardTitle, CardBody, Dropdown, DropdownList, DropdownItem, MenuToggle, Flex } from '../..'; + +import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon'; +import ServerAltIcon from '@patternfly/react-icons/dist/esm/icons/server-alt-icon'; + +import { + Chart, + ChartArea, + ChartAxis, + ChartGroup, + ChartThemeColor, + ChartVoronoiContainer +} from '@patternfly/react-charts/dist/esm/victory/components'; + +export const NetworkActivityCard: React.FunctionComponent = () => { + const [isKebabOpen, setIsKebabOpen] = useState(false); + + // Data for the chart + const chartData = [ + { name: 'Network Activity', x: '1', y: 10 }, + { name: 'Network Activity', x: '2', y: 8 }, + { name: 'Network Activity', x: '3', y: 6 }, + { name: 'Network Activity', x: '4', y: 5 }, + { name: 'Network Activity', x: '5', y: 5.5 }, + { name: 'Network Activity', x: '6', y: 5 }, + { name: 'Network Activity', x: '7', y: 4 }, + { name: 'Network Activity', x: '8', y: 4.5 }, + { name: 'Network Activity', x: '9', y: 4.5 }, + { name: 'Network Activity', x: '10', y: 0 } + ]; + + // Items for the kebab dropdown menu + const dropdownItems = [ + View details, + Refresh data, + Settings + ]; + + // The kebab menu toggle button + const kebabToggle = (toggleRef: React.Ref) => ( + setIsKebabOpen(!isKebabOpen)} + isExpanded={isKebabOpen} + aria-label="Network activity card kebab menu" + > + + + ); + + return ( + // To match the dark theme, we wrap the card in a div with a dark background. + // The `isPlain` and `isFlat` props on the Card remove its default styling. +
+ + setIsKebabOpen(false)} + onOpenChange={(isOpen: boolean) => setIsKebabOpen(isOpen)} + toggle={kebabToggle} + popperProps={{ position: 'right' }} + > + {dropdownItems} + + ), + hasNoOffset: false, + className: '' + }} + > + + + + Network activity + + + + + `${datum.name}: ${datum.y}`} constrainToVisibleArea /> + } + height={250} + padding={{ + bottom: 50, + left: 50, + right: 20, + top: 20 + }} + themeColor={ChartThemeColor.multi} + width={400} + > + + + + + + + + +
+ ); +}; +NetworkActivityCard.displayName = 'NetworkActivityCard'; + +export default NetworkActivityCard; diff --git a/packages/react-core/src/demos/Animations/AnimationsOverviewStorage.tsx b/packages/react-core/src/demos/Animations/AnimationsOverviewStorage.tsx new file mode 100644 index 00000000000..7c9b87ddda2 --- /dev/null +++ b/packages/react-core/src/demos/Animations/AnimationsOverviewStorage.tsx @@ -0,0 +1,111 @@ +import { useState } from 'react'; +import { + Card, + CardTitle, + CardBody, + CardFooter, + Flex, + Button, + Dropdown, + DropdownItem, + MenuToggle, + CardHeader, + DropdownList, + FlexItem +} from '../..'; +import { ChartDonutThreshold, ChartDonutUtilization } from '@patternfly/react-charts/dist/esm/victory/components'; +import CubesIcon from '@patternfly/react-icons/dist/esm/icons/cubes-icon'; +import ArrowRightIcon from '@patternfly/react-icons/dist/esm/icons/arrow-right-icon'; +import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon'; + +export const StorageCard: React.FunctionComponent = () => { + const [isKebabOpen, setIsKebabOpen] = useState(false); + + const dropdownItems = [ + Action 1, + Action 2, + Action 3 + ]; + + // The kebab menu toggle button + const kebabToggle = (toggleRef: React.Ref) => ( + setIsKebabOpen(!isKebabOpen)} + isExpanded={isKebabOpen} + aria-label="Cluster inventory card kebab menu" + > + + + ); + + return ( + + setIsKebabOpen(false)} + onOpenChange={(isOpen: boolean) => setIsKebabOpen(isOpen)} + toggle={kebabToggle} + popperProps={{ position: 'right' }} + > + {dropdownItems} + + ), + hasNoOffset: false, + className: '' + }} + > + + + + Storage + + + + + + + (datum.x ? datum.x : null)} + padding={{ + bottom: 0, + left: 10, + right: 10, + top: 0 + }} + width={200} + > + (datum.x ? `${datum.x}: ${datum.y}%` : null)} + title="80%" + subTitle="CPU" + thresholds={[{ value: 60 }, { value: 90 }]} + /> + + + + + + + + + ); +}; +StorageCard.displayName = 'StorageCard'; + +export default StorageCard; diff --git a/packages/react-core/src/demos/Animations/AnimationsStartTourModal.tsx b/packages/react-core/src/demos/Animations/AnimationsStartTourModal.tsx new file mode 100644 index 00000000000..e153bd14aa2 --- /dev/null +++ b/packages/react-core/src/demos/Animations/AnimationsStartTourModal.tsx @@ -0,0 +1,32 @@ +import { FunctionComponent } from 'react'; +import { Button, Content, Modal, ModalBody, ModalFooter, ModalHeader, ModalVariant } from '../../index'; + +interface Props { + onClose: (startTour?: boolean) => void; +} + +export const AnimationsStartTourModal: FunctionComponent = ({ onClose }) => ( + onClose()} + aria-labelledby="guided-tour-title" + aria-describedby="guided-tour-description" + > + + + + Welcome! Many of our components now use motion to engage users, provide clear feedback, and improve usability. + Let's explore some of these new animations and see how they work in a real UI. + + + + + + + +); diff --git a/packages/react-core/src/demos/Animations/GuidedTourContext.tsx b/packages/react-core/src/demos/Animations/GuidedTourContext.tsx new file mode 100644 index 00000000000..f735d6f56bb --- /dev/null +++ b/packages/react-core/src/demos/Animations/GuidedTourContext.tsx @@ -0,0 +1,202 @@ +import { createContext, useContext, useCallback, useEffect, useState, useRef } from 'react'; +import { Button, ButtonVariant, debounce, Flex, FlexItem, getResizeObserver, Popover } from '../..'; +import TimesIcon from '@patternfly/react-icons/dist/esm/icons/times-icon'; +import { GuidedTourStep } from './types'; +import Spotlight from './Spotlight'; + +interface GuidedTourContextType { + onStart: () => void; + onNextStep: () => void; + onPrevStep: () => void; + onFinish: () => void; + tourStep: GuidedTourStep | undefined; + isFinished: boolean; + setCustomStepContent: (customContent: React.ReactNode) => void; + renderTourStepElement: (forStepId: string, child: React.ReactElement) => React.ReactElement; +} + +const GuidedTourContext = createContext({ + onStart: () => {}, + onNextStep: () => {}, + onPrevStep: () => {}, + onFinish: () => {}, + setCustomStepContent: () => {}, + renderTourStepElement: () => null, + tourStep: undefined, + isFinished: false +}); + +export const GuidedTourProvider: React.FC<{ steps: GuidedTourStep[]; children: React.ReactNode }> = ({ + steps, + children +}) => { + const [currentStep, setCurrentStep] = useState(); + const [customStepContent, setCustomStepContent] = useState(); + const [windowWidth, setWindowWidth] = useState(); + const unObserver = useRef(null); + + const isMobile = windowWidth < 500; + + useEffect(() => { + setCurrentStep(undefined); + setCustomStepContent(undefined); + }, [steps]); + + const onStart = useCallback(() => { + setCustomStepContent(undefined); + setCurrentStep(0); + }, []); + + const onFinish = useCallback(() => { + setCustomStepContent(undefined); + setCurrentStep(undefined); + }, []); + + const onNextStep = useCallback(() => { + setCustomStepContent(undefined); + setCurrentStep((prev) => { + if (prev === undefined) { + return prev; + } + return prev + 1; + }); + }, []); + + const onPrevStep = useCallback(() => { + setCustomStepContent(undefined); + setCurrentStep((prev) => { + if (prev === undefined || prev === 0) { + return prev; + } + return prev - 1; + }); + }, []); + + const tourStep = currentStep !== undefined ? steps[currentStep] : undefined; + const isFinished = currentStep !== undefined ? currentStep >= steps.length : false; + + const renderTourStepElement = useCallback( + (forStepId: string, child: React.ReactElement) => { + if (!tourStep || forStepId !== tourStep.stepId) { + return child; + } + return ( + <> + {tourStep.spotlightSelector ? ( + + ) : null} + + {tourStep.header} + { + // Had to add a close button here rather than using the showClose property to include the close button + // Using the provided close button requires the 'shouldClose' property to handle the close click, but it also + // gets called on a triggerRef click which we don't want since we ask the user to click the button in order + // to see the animation. I don't see how to distinguish between the close button click and the triggerRef click. + } +
+
+ + } + bodyContent={customStepContent || tourStep.content} + footerContent={ + + + Step {currentStep + 1}/{steps.length} + + + + + + + + + + + + + } + > + {child} +
+ + ); + }, + [tourStep, currentStep, steps, onNextStep, onPrevStep, onFinish, customStepContent, isMobile] + ); + + const measureRef = (ref: HTMLDivElement) => { + // Remove any previous observer + if (unObserver.current) { + unObserver.current(); + } + + if (!ref) { + return; + } + + const handleResize = () => setWindowWidth(ref.clientWidth); + + // Set size on initialization + handleResize(); + + const debounceResize = debounce(handleResize, 100); + + // Update graph size on resize events + unObserver.current = getResizeObserver(ref, debounceResize); + }; + + useEffect( + () => () => { + if (unObserver.current) { + unObserver.current(); + } + }, + [] + ); + + return ( + +
+ {children} + + ); +}; +GuidedTourProvider.displayName = 'GuidedTourProvider'; + +export const useGuidedTour = (): GuidedTourContextType => { + const context = useContext(GuidedTourContext); + + if (!context) { + throw new Error('useGuidedTour must be used within a GuidedTourProvider'); + } + return context; +}; diff --git a/packages/react-core/src/demos/Animations/Spotlight.tsx b/packages/react-core/src/demos/Animations/Spotlight.tsx new file mode 100644 index 00000000000..2840bda6c38 --- /dev/null +++ b/packages/react-core/src/demos/Animations/Spotlight.tsx @@ -0,0 +1,80 @@ +import { useEffect, useRef, useState } from 'react'; +import { debounce, getResizeObserver } from '../..'; + +const SpotlightBorderWidth = 3; +const SpotlightGap = 4; + +type BoundingClientRect = ClientRect | null; + +interface SpotlightProps { + selector: string; + resizeSelector?: string; +} + +const Spotlight: React.FC = ({ selector, resizeSelector }) => { + const [clientRect, setClientRect] = useState( + document.querySelector(selector)?.getBoundingClientRect() + ); + const unObserver = useRef(null); + + // if target element is a hidden one return null + const element = document.querySelector(selector); + + useEffect(() => { + if (!element) { + return; + } + + const handleResize = () => { + if (element) { + setClientRect(element.getBoundingClientRect()); + } + }; + + const debounceResize = debounce(handleResize, 100); + + // Update graph size on resize events + const resizeElement = resizeSelector ? document.querySelector(resizeSelector) || element : element; + unObserver.current = getResizeObserver(resizeElement, debounceResize); + + return () => { + if (unObserver.current) { + unObserver.current(); + unObserver.current = undefined; + } + }; + }, [element, resizeSelector]); + + useEffect( + () => () => { + if (unObserver.current) { + unObserver.current(); + unObserver.current = undefined; + } + }, + [] + ); + + if (!element) { + return null; + } + + const style: React.CSSProperties = clientRect + ? { + position: 'fixed', + top: clientRect.top - (SpotlightBorderWidth + SpotlightGap), + left: clientRect.left - (SpotlightBorderWidth + SpotlightGap), + height: clientRect.height + 2 * (SpotlightBorderWidth + SpotlightGap), + width: clientRect.width + 2 * (SpotlightBorderWidth + SpotlightGap), + borderWidth: 3, + borderStyle: 'solid', + borderColor: 'var(--pf-t--global--background--color--highlight--default)', + background: 'transparent', + pointerEvents: 'none' + } + : {}; + + return clientRect ?
: null; +}; + +export default Spotlight; diff --git a/packages/react-core/src/demos/Animations/examples/Animations.tsx b/packages/react-core/src/demos/Animations/examples/Animations.tsx new file mode 100644 index 00000000000..f25921e2776 --- /dev/null +++ b/packages/react-core/src/demos/Animations/examples/Animations.tsx @@ -0,0 +1,867 @@ +import { useRef, useState, useEffect, FunctionComponent, FormEvent, useCallback } from 'react'; +import { + AlertGroup, + Alert, + AlertVariant, + AlertActionCloseButton, + Brand, + Button, + Content, + Card, + CardBody, + CardHeader, + CardTitle, + ContentVariants, + debounce, + EmptyState, + EmptyStateActions, + EmptyStateBody, + EmptyStateFooter, + getResizeObserver, + Label, + Masthead, + MastheadMain, + MastheadBrand, + MastheadToggle, + MastheadContent, + MastheadLogo, + Nav, + NavItem, + NavList, + NavExpandable, + Page, + PageSection, + PageSidebar, + PageSidebarBody, + PageToggleButton, + SkipToContent, + Tabs, + Tab, + TabTitleText, + Dropdown, + DropdownList, + DropdownItem, + MenuToggle, + Flex, + ProgressStepper, + ProgressStep, + Spinner +} from '@patternfly/react-core'; +import { Table, Thead, Tbody, Tr, Th, Td, ExpandableRowContent } from '@patternfly/react-table'; +import SkeletonTable from '@patternfly/react-component-groups/dist/dynamic/SkeletonTable'; +import BoltIcon from '@patternfly/react-icons/dist/esm/icons/bolt-icon'; +import EllipsisVIcon from '@patternfly/react-icons/dist/esm/icons/ellipsis-v-icon'; +import ExclamationCircleIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon'; +import CheckCircleIcon from '@patternfly/react-icons/dist/esm/icons/check-circle-icon'; +import ExclamationTriangleIcon from '@patternfly/react-icons/dist/esm/icons/exclamation-triangle-icon'; +import ResourcesFullIcon from '@patternfly/react-icons/dist/esm/icons/resources-full-icon'; +// @ts-ignore +import pfLogo from '@patternfly/react-core/src/demos/assets/pf-logo.PF-HorizontalLogo-Color.svg'; +import { Application, GuidedTourStep, NotificationType } from '../types'; +import { AnimationsOverview } from '../../../../dist/esm/demos/Animations/AnimationsOverview'; +import { AnimationsNotificationsDrawer } from '../../../../dist/esm/demos/Animations/AnimationsNotificationsDrawer'; +import { AnimationsCreateDatabaseForm } from '../../../../dist/esm/demos/Animations/AnimationsCreateDatabaseForm'; +import { GuidedTourProvider, useGuidedTour } from '../../../../dist/esm/demos/Animations/GuidedTourContext'; +import { AnimationsHeaderToolbar } from '../../../../dist/esm/demos/Animations/AnimationsHeaderToolbar'; +import { AnimationsStartTourModal } from '../../../../dist/esm/demos/Animations/AnimationsStartTourModal'; +import { AnimationsEndTourModal } from '../../../../dist/esm/demos/Animations/AnimationsEndTourModal'; +import { applicationsData } from './ResourceTableData'; +import openshiftLogo from '../../assets/Summit-collage-depoying-openshift-product-icon-RH.png'; +import emptyStateLogo from '../../assets/Summit-collage-hybrid-cloud-dark-RH.png'; + +// Simple component to wrap the empty state logo +const EmptyStateLogoIcon: React.FunctionComponent = () => ( + Empty state +); + +const mainContainerPageId = 'main-content-page-layout-default-nav'; +const expandableColumns = ['Applications', 'Server', 'Branch', 'Status']; +const initialExpandedServerNames = ['Cost Management']; // Default to expanded + +export const GuidedTourSteps: GuidedTourStep[] = [ + { + stepId: 'toastNotifications', + header:
Alerts
, + content: '===== This content is customized ======' + }, + { + stepId: 'settingsButton', + header:
Buttons: Settings
, + content: '===== This content is customized ======', + spotlightSelector: '#settings-button' + }, + { + stepId: 'navToggle', + header:
Buttons: Hamburger menu
, + content: ( + <> + Hover over the hamburger menu to see an arrow indicator appear. + + Click the button and watch the arrow's direction change as the menu opens and closes, always showing you what + will happen next. + + + ), + spotlightSelector: '#nav-toggle' + }, + { + stepId: 'notificationBadge', + header:
Buttons: Notification badge
, + content: '===== This content is customized ======', + spotlightSelector: '#notification-badge' + }, + { + stepId: 'tabs', + header:
Tabs
, + position: 'top', + content: ( + + Click between the different tabs and watch how the active tab indicator smoothly slides to your selection, + providing clear feedback on your location. + + ), + spotlightSelector: '#tabs' + }, + { + stepId: 'skeletonLoader', + header:
Skeleton loader
, + position: 'top', + content: ( + <> + + Watch how the loading indicators animate to inform the user that there is processing going on behind the + scenes. + + + ), + spotlightSelector: '#skeleton-table' + }, + { + stepId: 'expandableComponents', + header:
Expandable components
, + position: 'top', + content: ( + <> + Click to expand this hidden content section. + + Notice how the hidden information smoothly fades and slides into place. Click again to collapse it and see the + reverse animation. + + Reduced-motion users will only see the fade, not the sliding motion. + + ), + spotlightSelector: '#expand-toggle-1' + }, + { + stepId: 'validationErrors', + header:
Validation errors
, + content: ( + <> + + Click Submit while fields are empty to trigger an error. Watch the input fields jiggle from + side to side, drawing your attention to issues that need fixing. + + Reduced-motion users will only see the fade, not the jiggle. + + ), + spotlightSelector: '#create-database-submit', + spotlightResizeSelector: '#create-database-form' + // }, + // { + // stepId: 'progressStepper', + // header:
In process indicator
, + // content: ( + // <> + // + // Watch as a process starts for step 2. + // + // + // When a task is running, the in-process icon now spins in place, providing clear and continuous feedback that the system is working. + // + // + // ) + } +]; + +const AnimationsPage: FunctionComponent = () => { + const drawerRef = useRef(null); + const [isDrawerExpanded, setIsDrawerExpanded] = useState(false); + const [notifications, setNotifications] = useState([ + { + id: 'notification-1', + title: 'Unread info notification title', + message: 'This is an info notification description.', + variant: AlertVariant.info, + timeout: 3000, + timeoutAnimation: 3000, + isNew: false, + isRead: false + }, + { + id: 'notification-2', + title: + 'Unread danger notification title. This is a long title to show how the title will wrap if it is long and wraps to multiple lines.', + message: + 'This is a danger notification description. This is a long description to show how the title will wrap if it is long and wraps to multiple lines.', + variant: AlertVariant.danger, + timeout: 3000, + timeoutAnimation: 3000, + isNew: false, + isRead: true + }, + { + id: 'notification-3', + title: 'Read warning notification title', + message: 'This is a warning notification description.', + variant: AlertVariant.warning, + timeout: 3000, + timeoutAnimation: 3000, + isNew: false, + isRead: true + }, + { + id: 'notification-4', + title: 'Read success notification title', + message: 'This is a success notification description.', + variant: AlertVariant.success, + timeout: 3000, + timeoutAnimation: 3000, + isNew: false, + isRead: true + } + ]); + const [selectedTab, setSelectedTab] = useState(0); + const [showForm, setShowForm] = useState(false); + const [showStartTourModal, setShowStartTourModal] = useState(true); + const [showEndTourModal, setShowEndTourModal] = useState(false); + const [activeItem, setActiveItem] = useState(0); + const [activeGroup, setActiveGroup] = useState(null); + const { onStart, onFinish, renderTourStepElement, setCustomStepContent, tourStep, isFinished } = useGuidedTour(); + const [windowWidth, setWindowWidth] = useState(1200); + const unObserver = useRef(null); + + const isMobile = windowWidth < 500; + + // HERE + const [openKebabIndex, setOpenKebabIndex] = useState(-1); + + interface Activity { + id: number; + name: string; + project: string; + // Each step is represented by its variant + progress: ('success' | 'info' | 'pending' | 'warning' | 'danger' | 'default')[]; + } + + // Data for the table rows + const activityData: Activity[] = [ + { id: 1, name: 'my-pod-name', project: 'project-test', progress: ['success', 'success', 'success'] }, + { id: 2, name: 'my-pod-name', project: 'project-test', progress: ['success', 'pending', 'default'] }, + { id: 3, name: 'my-pod-name', project: 'project-test', progress: ['success', 'success', 'danger'] }, + { id: 4, name: 'my-pod-name', project: 'project-test', progress: ['success', 'warning', 'pending'] } + ]; + + const dropdownItems = [ + View details, + Monitor activity, + Delete + ]; + + // A function to create a kebab toggle for a specific row index + const kebabToggle = (index) => (toggleRef: React.Ref) => ( + setOpenKebabIndex(openKebabIndex === index ? -1 : index)} + isExpanded={openKebabIndex === index} + aria-label={`Kebab menu for row ${index}`} + > + + + ); + + const iconMap = { + success: , + info: , + pending: , + danger: , + warning: + }; + + const recentActivityCard = ( + + setOpenKebabIndex(-1)} + onOpenChange={(isOpen: boolean) => !isOpen && setOpenKebabIndex(-1)} + toggle={kebabToggle(-2, true)} + popperProps={{ position: 'right' }} + > + {dropdownItems} + + ), + hasNoOffset: false, + className: '' + }} + > + + + + Recent activity + + + + + + + + + + + + + + {activityData.map((activity, rowIndex) => ( + + + + + + + ))} + +
NameProjectProgress +
+ + + + + + {activity.progress.map((stepVariant, stepIndex) => ( + + ))} + + + setOpenKebabIndex(-1)} + onOpenChange={(isOpen: boolean) => !isOpen && setOpenKebabIndex(-1)} + toggle={kebabToggle(rowIndex)} + popperProps={{ position: 'right' }} + > + {dropdownItems} + +
+
+
+ ); + + const addNotification = useCallback((showToast = true) => { + setNotifications((prev) => [ + { + id: `new-notification-${prev.length + 1}`, + title: 'Animated notification', + message: 'A system alert has been triggered. Please review the alert details.', + variant: AlertVariant.danger, + timeout: 3000, + timeoutAnimation: 3000, + isNew: showToast, + isRead: false + }, + ...prev + ]); + }, []); + + useEffect(() => { + if (tourStep?.stepId === 'toastNotifications') { + setCustomStepContent( + <> + + Click Add alert. In a moment, a new toast alert will appear. + + + Notice how it slides smoothly into view to draw your eye to critical information, and then slides out just + as smoothly once it expires. + + + + + + ); + } + if (tourStep?.stepId === 'settingsButton') { + setCustomStepContent( + <> + {!isMobile ? ( + + Hover over the settings button. The cog icon rotates to show that it’s interactive. + + ) : null} + + {`Click ${isMobile ? 'the settings button' : 'it'} to see the new ripple effect we've added to all buttons.`} + + + ); + } + + if (tourStep?.stepId === 'notificationBadge') { + setCustomStepContent( + <> + + Click Add notification. Watch for a new notification to arrive. + + + The bell icon "rings" with a subtle rotation to quickly catch your attention as a message comes in. + + + + + + ); + } + if (tourStep?.stepId === 'expandableComponents' || tourStep?.stepId === 'skeletonLoader') { + setSelectedTab(1); + } + if (tourStep?.stepId === 'validationErrors') { + setSelectedTab(2); + setShowForm(true); + } + }, [tourStep?.stepId, setCustomStepContent, addNotification, isMobile]); + + useEffect(() => { + setShowEndTourModal(isFinished); + }, [isFinished]); + + const measureRef = (ref: HTMLDivElement) => { + // Remove any previous observer + if (unObserver.current) { + unObserver.current(); + } + + if (!ref) { + return; + } + + const handleResize = () => setWindowWidth(ref.clientWidth); + + // Set size on initialization + handleResize(); + + const debounceResize = debounce(handleResize, 100); + + // Update graph size on resize events + unObserver.current = getResizeObserver(ref, debounceResize); + }; + + useEffect( + () => () => { + if (unObserver.current) { + unObserver.current(); + } + }, + [] + ); + + const startNotifications = () => { + setTimeout(() => { + addNotification(); + }, 1000); + setTimeout(() => { + addNotification(); + }, 5000); + setTimeout(() => { + addNotification(); + }, 9000); + }; + + const onNavSelect = ( + _event: FormEvent, + selectedItem: { + groupId: number | string | null; + itemId: number | string; + to: string; + } + ) => { + setActiveItem(selectedItem.itemId); + setActiveGroup(selectedItem.groupId as string | null); + }; + + const focusDrawer = (_event: any) => { + if (drawerRef.current === null) { + return; + } + const firstTabbableItem = drawerRef.current.querySelector('a, button') as + | HTMLAnchorElement + | HTMLButtonElement + | null; + firstTabbableItem?.focus(); + }; + + const closeStartTourModal = (startTour = false) => { + setShowStartTourModal(false); + startTour ? onStart() : startNotifications(); + }; + + return ( + + + + {renderTourStepElement( + 'navToggle', + + )} + + + + + + + + + setShowStartTourModal(true)} + onEndGuidedTour={() => onFinish()} + /> + + + } + sidebar={ + + + + + + } + isManagedSidebar + notificationDrawer={ + setIsDrawerExpanded(false)} + /> + } + onNotificationDrawerExpand={(event) => focusDrawer(event)} + isNotificationDrawerExpanded={isDrawerExpanded} + skipToContent={ + { + event.preventDefault(); + + const mainContentElement = document.getElementById(mainContainerPageId); + if (mainContentElement) { + mainContentElement.focus(); + } + }} + href={`#${mainContainerPageId}`} + > + Skip to content + + } + mainContainerId={mainContainerPageId} + > +
+ + {renderTourStepElement( + 'toastNotifications', +
+ )} + {notifications + .filter((n) => n.isNew) + .map((alert) => ( + + { + setNotifications((prev) => + prev.reduce((acc: NotificationType[], next) => { + if (next.id === alert.id) { + acc.push({ ...next, isNew: false }); + } else { + acc.push(next); + } + return acc; + }, []) + ); + }} + actionClose={ {}} />} + > + {alert.message} + + + ))} + Resources + Everything you need to know about your application + {renderTourStepElement( + 'tabs', + setSelectedTab(Number(key))} + aria-label="Primary tabs" + > + Overview} tabContentId="overview" /> + Resources} tabContentId="resources" /> + Database} tabContentId="database" /> + + )} + + {selectedTab === 0 && ( + + )} + + {selectedTab === 1 && ( + + + + )} + + {selectedTab === 2 && ( + + {showForm ? ( + setShowForm(false)} /> + ) : ( + + No results match the filter criteria. Clear all filters and try again. + + + + + + + )} + + )} + {showStartTourModal ? : null} + {showEndTourModal ? : null} + + ); +}; + +// Can't break this into a separate file, seems we need to stay in the examples dir when using '@patternfly/react-table' +const AnimationsResourcesTable: FunctionComponent = () => { + const [expandedAppNames, setExpandedAppNames] = useState(initialExpandedServerNames); + const [loading, setLoading] = useState(true); + const { tourStep, renderTourStepElement } = useGuidedTour(); + + // --- 1. ADD STATE FOR FAVORITES --- + const [favoriteAppNames, setFavoriteAppNames] = useState([]); + const [headerFavorited, setHeaderFavorited] = useState(false); + + // --- 2. ADD HELPER FUNCTIONS FOR FAVORITES --- + const setAppFavorited = (app: Application, isFavoriting = true) => + setFavoriteAppNames((prevFavorites) => { + const otherFavorites = prevFavorites.filter((r) => r !== app.name); + return isFavoriting ? [...otherFavorites, app.name] : otherFavorites; + }); + + const isAppFavorited = (app: Application) => favoriteAppNames.includes(app.name); + + const getSortParams = (columnIndex) => ({ + isFavorites: columnIndex === 0, + sortBy: { + index: undefined, + direction: undefined + }, + onSort: (_event) => { + // No sorting logic needed, just favorites + }, + 'aria-label': 'Sort favorites', + columnIndex, + favoriteButtonProps: { + favorited: headerFavorited, + onClick: (_event) => { + applicationsData.forEach((app) => setAppFavorited(app, !headerFavorited)); + setHeaderFavorited(!headerFavorited); + }, + 'aria-label': headerFavorited ? 'Unfavorite all' : 'Favorite all', + isFavorite: true, + isFavorited: headerFavorited, + variant: 'plain' + } + }); + + // --- Existing hooks and functions --- + useEffect(() => { + const timer = setTimeout(() => setLoading(false), 2000); + return () => { + if (timer) { + clearTimeout(timer); + } + }; + }, [loading, tourStep?.stepId]); + + const setAppExpanded = (app: Application, isExpanding: boolean) => { + const others = expandedAppNames.filter((n) => n !== app.name); + setExpandedAppNames(isExpanding ? [...others, app.name] : others); + }; + + const isAppExpanded = (app: Application) => expandedAppNames.includes(app.name); + + return ( + <> + {loading || tourStep?.stepId === 'skeletonLoader' ? ( + <> + {renderTourStepElement( + 'skeletonLoader', + + )} + + ) : ( + + + {renderTourStepElement( + 'expandableComponents', +
+ )} +
+ {/* Move favorite column to first position */} + + ))} + + + + {/* Use separate Tbody for each expandable group like the working example */} + {applicationsData.map((app, idx) => ( + + + {/* Move favorite to first position */} + + {/* Move expand to second position */} + + + + + + + + + + ))} +
+ {/* Move expand column to second position */} + + {expandableColumns.map((column) => ( + {column}
+ setAppExpanded(app, !isAppExpanded(app)) + } + : undefined + } + /> + {app.name}{app.header}{app.branch} + {app.status === 'Running' && } + {app.status === 'Degraded' && } + {app.status === 'Stopped' && } + {app.status !== 'Running' && app.status !== 'Degraded' && app.status !== 'Stopped' && app.status} +
+ + + {app.details} +
+ )} + + ); +}; + +export const Animations: FunctionComponent = () => ( + + + +); diff --git a/packages/react-core/src/demos/Animations/examples/ResourceTableData.jsx b/packages/react-core/src/demos/Animations/examples/ResourceTableData.jsx new file mode 100644 index 00000000000..ca43cd7003d --- /dev/null +++ b/packages/react-core/src/demos/Animations/examples/ResourceTableData.jsx @@ -0,0 +1,85 @@ +import React from 'react'; + +export const applicationsData = [ + { + name: 'Cost Management', + header: 'US-East-1', + branch: 'main', + status: 'Running', + details:

Monitors cloud spending across all services. Last updated 1 hour ago.

+ }, + { + name: 'User Authentication', + header: 'EU-West-2', + branch: 'dev', + status: 'Degraded', + details: ( +
+

OAuth 2.0 provider is reporting high latency. SAML provider is operational.

+
+ ) + }, + { + name: 'Data Processing', + header: 'US-West-1', + branch: 'feature-new-parser', + status: 'Stopped', + details:

Stopped pending deployment of new data schema. Do not restart manually.

+ }, + { + name: 'Inventory API', + header: 'US-East-1', + branch: 'main', + status: 'Running', + details:

Provides read/write access to the product inventory database.

+ }, + { + name: 'Frontend Web App', + header: 'APAC-Tokyo', + branch: 'release-v2.5.1', + status: 'Degraded', + details:

A new vulnerability (CVE-2025-12345) was detected in a dependency.

+ }, + { + name: 'Logging Service', + header: 'EU-Central-1', + branch: 'hotfix-log-rotation', + status: 'Running', + details:

Aggregating logs from all production services. Current volume: 2,500 logs/min.

+ }, + { + name: 'API Gateway', + header: 'US-East-1', + branch: 'main', + status: 'Degraded', + details:

High latency detected on the `/v2/query` endpoint. Please investigate.

+ }, + { + name: 'Notification Queue', + header: 'US-West-2', + branch: 'dev', + status: 'Running', + details:

Currently processing a backlog of 1,500 messages. Estimated time to clear: 15 minutes.

+ }, + { + name: 'Billing Processor', + header: 'EU-West-1', + branch: 'main', + status: 'Stopped', + details:

Service is stopped pending end-of-month financial reconciliation.

+ }, + { + name: 'Content Delivery Network', + header: 'Global', + branch: 'config-update', + status: 'Running', + details:

Serving assets globally with 58 points of presence. Cache hit ratio: 98.2%.

+ }, + { + name: 'Reporting Dashboard', + header: 'US-East-2', + branch: 'feature-new-charts', + status: 'Degraded', + details:

Data source `reporting-db-replica` is out of sync. Reports may be stale.

+ } +]; \ No newline at end of file diff --git a/packages/react-core/src/demos/Animations/types.ts b/packages/react-core/src/demos/Animations/types.ts new file mode 100644 index 00000000000..fed01f7aeed --- /dev/null +++ b/packages/react-core/src/demos/Animations/types.ts @@ -0,0 +1,30 @@ +import { AlertVariant } from '../..'; +import { ReactNode } from 'react'; + +export interface NotificationType { + id: string; + title: string; + message: string; + variant: AlertVariant; + timeout: number; + timeoutAnimation: number; + isNew: boolean; + isRead: boolean; +} + +export interface Application { + name: string; + header: string; + branch: string; + status: string; + details?: ReactNode; +} + +export interface GuidedTourStep { + stepId: string; + header: React.ReactNode; + content: React.ReactNode; + position?: 'auto' | 'top' | 'bottom' | 'left' | 'right'; + spotlightSelector?: string; + spotlightResizeSelector?: string; +} diff --git a/packages/react-core/src/demos/assets/Summit-collage-depoying-openshift-product-icon-RH.png b/packages/react-core/src/demos/assets/Summit-collage-depoying-openshift-product-icon-RH.png new file mode 100644 index 00000000000..e57dc3ba7e0 Binary files /dev/null and b/packages/react-core/src/demos/assets/Summit-collage-depoying-openshift-product-icon-RH.png differ diff --git a/packages/react-core/src/demos/assets/Summit-collage-hybrid-cloud-dark-RH.png b/packages/react-core/src/demos/assets/Summit-collage-hybrid-cloud-dark-RH.png new file mode 100644 index 00000000000..0966f47bb9c Binary files /dev/null and b/packages/react-core/src/demos/assets/Summit-collage-hybrid-cloud-dark-RH.png differ diff --git a/packages/react-core/src/demos/assets/index.d.ts b/packages/react-core/src/demos/assets/index.d.ts index cdb2b1a9a23..c917e38cc2f 100644 --- a/packages/react-core/src/demos/assets/index.d.ts +++ b/packages/react-core/src/demos/assets/index.d.ts @@ -2,3 +2,8 @@ declare module '*.svg' { const content: string; export default content; } + +declare module '*.png' { + const content: string; + export default content; +} diff --git a/packages/react-docs/patternfly-docs/generated/design-foundations/motion/demo/animations.png b/packages/react-docs/patternfly-docs/generated/design-foundations/motion/demo/animations.png new file mode 100644 index 00000000000..c47c5e5ab1a Binary files /dev/null and b/packages/react-docs/patternfly-docs/generated/design-foundations/motion/demo/animations.png differ diff --git a/packages/react-docs/patternfly-docs/patternfly-docs.config.js b/packages/react-docs/patternfly-docs/patternfly-docs.config.js index c7e99dd0c76..5e032f2047e 100644 --- a/packages/react-docs/patternfly-docs/patternfly-docs.config.js +++ b/packages/react-docs/patternfly-docs/patternfly-docs.config.js @@ -9,6 +9,7 @@ module.exports = { hasDesignGuidelines: false, sideNavItems: [ { section: 'get-started' }, + { section: 'design-foundations' }, { section: 'developer-resources' }, { section: 'charts' }, { section: 'components' }, diff --git a/yarn.lock b/yarn.lock index 469a2ae1adc..0c4e5c444e3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1707,6 +1707,13 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.8.3": + version: 7.27.6 + resolution: "@babel/runtime@npm:7.27.6" + checksum: 10c0/89726be83f356f511dcdb74d3ea4d873a5f0cf0017d4530cb53aa27380c01ca102d573eff8b8b77815e624b1f8c24e7f0311834ad4fb632c90a770fda00bd4c8 + languageName: node + linkType: hard + "@babel/template@npm:^7.10.4, @babel/template@npm:^7.24.7, @babel/template@npm:^7.3.3": version: 7.24.7 resolution: "@babel/template@npm:7.24.7" @@ -1939,6 +1946,22 @@ __metadata: languageName: node linkType: hard +"@emotion/is-prop-valid@npm:^0.7.3": + version: 0.7.3 + resolution: "@emotion/is-prop-valid@npm:0.7.3" + dependencies: + "@emotion/memoize": "npm:0.7.1" + checksum: 10c0/6b0ef52435578c83e0bb8711c2f8397680c3c73c6c4d017f5eda7dcfbb3b4c096a7b9ef2ebe80d139fafb3aff3186b2f9de4024544245275a24c75dae7293d9e + languageName: node + linkType: hard + +"@emotion/memoize@npm:0.7.1": + version: 0.7.1 + resolution: "@emotion/memoize@npm:0.7.1" + checksum: 10c0/e93f8688df9ce19e152e3996cebb7dd915009815ed90136cb65383a3626e8ef5ee1ceee9e9978edfb351e23f2efa29d2ba9bd80b7384778385e7545db4d12d26 + languageName: node + linkType: hard + "@esbuild/aix-ppc64@npm:0.25.0": version: 0.25.0 resolution: "@esbuild/aix-ppc64@npm:0.25.0" @@ -3736,6 +3759,39 @@ __metadata: languageName: unknown linkType: soft +"@patternfly/react-component-groups@npm:^6.2.1": + version: 6.2.1 + resolution: "@patternfly/react-component-groups@npm:6.2.1" + dependencies: + "@patternfly/react-core": "npm:^6.0.0" + "@patternfly/react-icons": "npm:^6.0.0" + "@patternfly/react-table": "npm:^6.0.0" + clsx: "npm:^2.1.1" + react-jss: "npm:^10.10.0" + peerDependencies: + react: ^17 || ^18 + react-dom: ^17 || ^18 + checksum: 10c0/1673f230e532155b5f0ec2685e3a294ac3f40d8a43628e50c034c4fd0f37147fb4cfe23659f50de32a9caffea0472d1982a9974549ebe9db6ff89fb16e343c61 + languageName: node + linkType: hard + +"@patternfly/react-core@npm:^6.0.0, @patternfly/react-core@npm:^6.2.2": + version: 6.2.2 + resolution: "@patternfly/react-core@npm:6.2.2" + dependencies: + "@patternfly/react-icons": "npm:^6.2.2" + "@patternfly/react-styles": "npm:^6.2.2" + "@patternfly/react-tokens": "npm:^6.2.2" + focus-trap: "npm:7.6.4" + react-dropzone: "npm:^14.3.5" + tslib: "npm:^2.8.1" + peerDependencies: + react: ^17 || ^18 + react-dom: ^17 || ^18 + checksum: 10c0/a75facefb5111cbb90b00f9a616b04e88740a46a113455eefc0722b5a05851f32453f7fd29b281584dfc8536da0972f1fad890ee23075adcbd8c84e8aca90eb9 + languageName: node + linkType: hard + "@patternfly/react-core@workspace:^, @patternfly/react-core@workspace:packages/react-core": version: 0.0.0-use.local resolution: "@patternfly/react-core@workspace:packages/react-core" @@ -3794,6 +3850,16 @@ __metadata: languageName: unknown linkType: soft +"@patternfly/react-icons@npm:^6.0.0, @patternfly/react-icons@npm:^6.2.2": + version: 6.2.2 + resolution: "@patternfly/react-icons@npm:6.2.2" + peerDependencies: + react: ^17 || ^18 + react-dom: ^17 || ^18 + checksum: 10c0/3dfeb3606f80ee1e047b8d3bac88782627be19c49cad7af902d42af104a36cad70991ce186bdefa37c3ac84b9f55355b5ad88350bb5a0824b7f37654a68cac01 + languageName: node + linkType: hard + "@patternfly/react-icons@workspace:^, @patternfly/react-icons@workspace:packages/react-icons": version: 0.0.0-use.local resolution: "@patternfly/react-icons@workspace:packages/react-icons" @@ -3831,6 +3897,7 @@ __metadata: "@eslint/compat": "npm:^1.2.8" "@eslint/js": "npm:^9.22.0" "@octokit/rest": "npm:^21.1.1" + "@patternfly/react-component-groups": "npm:^6.2.1" "@rollup/plugin-commonjs": "npm:^26.0.3" "@rollup/plugin-node-resolve": "npm:^15.3.1" "@rollup/plugin-replace": "npm:^5.0.7" @@ -3844,6 +3911,7 @@ __metadata: "@types/react": "npm:^18.3.18" "@types/react-dom": "npm:^18.3.5" babel-jest: "npm:^29.7.0" + clsx: "npm:^2.1.1" concurrently: "npm:^9.1.2" eslint: "npm:^9.22.0" eslint-config-prettier: "npm:^10.1.5" @@ -3868,6 +3936,7 @@ __metadata: publint: "npm:^0.3.9" react: "npm:^18.3.1" react-dom: "npm:^18.3.1" + react-jss: "npm:^10.10.0" rimraf: "npm:^6.0.1" rollup: "npm:^4.36.0" rollup-plugin-scss: "npm:^4.0.1" @@ -3881,6 +3950,13 @@ __metadata: languageName: unknown linkType: soft +"@patternfly/react-styles@npm:^6.2.2": + version: 6.2.2 + resolution: "@patternfly/react-styles@npm:6.2.2" + checksum: 10c0/4e4c7247738dbc86582f952bd52fdbb349c4c46b84ba160b9a1cfbff47845fe34e3a7c86d2de6e4659e5bd4b6d9af51475b53d02a1774465cd489ab0d6be2408 + languageName: node + linkType: hard + "@patternfly/react-styles@workspace:^, @patternfly/react-styles@workspace:packages/react-styles": version: 0.0.0-use.local resolution: "@patternfly/react-styles@workspace:packages/react-styles" @@ -3891,6 +3967,23 @@ __metadata: languageName: unknown linkType: soft +"@patternfly/react-table@npm:^6.0.0": + version: 6.2.2 + resolution: "@patternfly/react-table@npm:6.2.2" + dependencies: + "@patternfly/react-core": "npm:^6.2.2" + "@patternfly/react-icons": "npm:^6.2.2" + "@patternfly/react-styles": "npm:^6.2.2" + "@patternfly/react-tokens": "npm:^6.2.2" + lodash: "npm:^4.17.21" + tslib: "npm:^2.8.1" + peerDependencies: + react: ^17 || ^18 + react-dom: ^17 || ^18 + checksum: 10c0/6159880410a17c6a16c2497e741657ee68eea1ab68d4cacb442761edfe17b0c44da3f9064e744ce940cca5194070446251d27ce45b7be760e4d2253d0eef83ff + languageName: node + linkType: hard + "@patternfly/react-table@workspace:^, @patternfly/react-table@workspace:packages/react-table": version: 0.0.0-use.local resolution: "@patternfly/react-table@workspace:packages/react-table" @@ -3922,6 +4015,13 @@ __metadata: languageName: unknown linkType: soft +"@patternfly/react-tokens@npm:^6.2.2": + version: 6.2.2 + resolution: "@patternfly/react-tokens@npm:6.2.2" + checksum: 10c0/b1ecd0b70dd399635cb567dfc1ac59119c96387b5670163d3b57beb07a5d2ec463942d4fc4579af7c18400d8235db2db5df8fbacf9a4910bc31699ff933c273c + languageName: node + linkType: hard + "@patternfly/react-tokens@workspace:^, @patternfly/react-tokens@workspace:packages/react-tokens": version: 0.0.0-use.local resolution: "@patternfly/react-tokens@workspace:packages/react-tokens" @@ -7727,6 +7827,13 @@ __metadata: languageName: node linkType: hard +"clsx@npm:^2.1.1": + version: 2.1.1 + resolution: "clsx@npm:2.1.1" + checksum: 10c0/c4c8eb865f8c82baab07e71bfa8897c73454881c4f99d6bc81585aecd7c441746c1399d08363dc096c550cceaf97bd4ce1e8854e1771e9998d9f94c4fe075839 + languageName: node + linkType: hard + "cmd-shim@npm:6.0.3, cmd-shim@npm:^6.0.0": version: 6.0.3 resolution: "cmd-shim@npm:6.0.3" @@ -8429,6 +8536,17 @@ __metadata: languageName: node linkType: hard +"css-jss@npm:10.10.0": + version: 10.10.0 + resolution: "css-jss@npm:10.10.0" + dependencies: + "@babel/runtime": "npm:^7.3.1" + jss: "npm:^10.10.0" + jss-preset-default: "npm:^10.10.0" + checksum: 10c0/448b075382a5067554f7ca6cc22e433f5125fb9dc3e6c5ca45be9859b81506768eddb082620b2eb0b606f3d9b2756039a4ad278018003bd1858b154df7bfe4bd + languageName: node + linkType: hard + "css-loader@npm:6.7.3": version: 6.7.3 resolution: "css-loader@npm:6.7.3" @@ -8460,6 +8578,16 @@ __metadata: languageName: node linkType: hard +"css-vendor@npm:^2.0.8": + version: 2.0.8 + resolution: "css-vendor@npm:2.0.8" + dependencies: + "@babel/runtime": "npm:^7.8.3" + is-in-browser: "npm:^1.0.2" + checksum: 10c0/2538bc37adf72eb79781929dbb8c48e12c6a4b926594ad4134408b3000249f1a50d25be374f0e63f688c863368814aa6cc2e9ea11ea22a7309a7d966b281244c + languageName: node + linkType: hard + "css-what@npm:^6.0.1": version: 6.1.0 resolution: "css-what@npm:6.1.0" @@ -12152,7 +12280,7 @@ __metadata: languageName: node linkType: hard -"hoist-non-react-statics@npm:^3.3.2": +"hoist-non-react-statics@npm:^3.2.0, hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.2": version: 3.3.2 resolution: "hoist-non-react-statics@npm:3.3.2" dependencies: @@ -12509,6 +12637,13 @@ __metadata: languageName: node linkType: hard +"hyphenate-style-name@npm:^1.0.3": + version: 1.1.0 + resolution: "hyphenate-style-name@npm:1.1.0" + checksum: 10c0/bfe88deac2414a41a0d08811e277c8c098f23993d6a1eb17f14a0f11b54c4d42865a63d3cfe1914668eefb9a188e2de58f38b55a179a238fd1fef606893e194f + languageName: node + linkType: hard + "iconv-lite@npm:0.4.24, iconv-lite@npm:^0.4.24": version: 0.4.24 resolution: "iconv-lite@npm:0.4.24" @@ -13175,6 +13310,13 @@ __metadata: languageName: node linkType: hard +"is-in-browser@npm:^1.0.2, is-in-browser@npm:^1.1.3": + version: 1.1.3 + resolution: "is-in-browser@npm:1.1.3" + checksum: 10c0/87e6119a56ec3d84910eb6ad855b4a3ac05b242fc2bc2c28abbf978f76b5a834ec5622165035acaf2844a85856b1a0fbc12bd0cb1ce9e86314ebec675c6fe856 + languageName: node + linkType: hard + "is-inside-container@npm:^1.0.0": version: 1.0.0 resolution: "is-inside-container@npm:1.0.0" @@ -14660,6 +14802,168 @@ __metadata: languageName: node linkType: hard +"jss-plugin-camel-case@npm:10.10.0": + version: 10.10.0 + resolution: "jss-plugin-camel-case@npm:10.10.0" + dependencies: + "@babel/runtime": "npm:^7.3.1" + hyphenate-style-name: "npm:^1.0.3" + jss: "npm:10.10.0" + checksum: 10c0/29dedf0866837425258eae3b12b72c1de435ea7caddef94ac13044b3a04c4abd8dd238a81fd6e0a4afdbf10c9cb4674df41f50af79554c34c736cd2ecf3752da + languageName: node + linkType: hard + +"jss-plugin-compose@npm:10.10.0": + version: 10.10.0 + resolution: "jss-plugin-compose@npm:10.10.0" + dependencies: + "@babel/runtime": "npm:^7.3.1" + jss: "npm:10.10.0" + tiny-warning: "npm:^1.0.2" + checksum: 10c0/41cbb79e788aa38422b576490078b8aab76a5931caa88013bbe96d2aaee7c8531114cc4d1aa300eb85d2d24566f75463d6f4f09018c1bdf15211622b0562247f + languageName: node + linkType: hard + +"jss-plugin-default-unit@npm:10.10.0": + version: 10.10.0 + resolution: "jss-plugin-default-unit@npm:10.10.0" + dependencies: + "@babel/runtime": "npm:^7.3.1" + jss: "npm:10.10.0" + checksum: 10c0/f394d5411114fde7056249f4650de51e6f3e47c64a3d48cee80180a6e75876f0d0d68c96d81458880e1024ca880ed53baade682d36a5f7177046bfef0b280572 + languageName: node + linkType: hard + +"jss-plugin-expand@npm:10.10.0": + version: 10.10.0 + resolution: "jss-plugin-expand@npm:10.10.0" + dependencies: + "@babel/runtime": "npm:^7.3.1" + jss: "npm:10.10.0" + checksum: 10c0/5673c529aab837cd62860f7daa82a4b9ed1ad82b961d19e55ff529e86da47effebd8b922993d2dfaa88290aaf6351592728e6a74d0780fffab2f383613316d38 + languageName: node + linkType: hard + +"jss-plugin-extend@npm:10.10.0": + version: 10.10.0 + resolution: "jss-plugin-extend@npm:10.10.0" + dependencies: + "@babel/runtime": "npm:^7.3.1" + jss: "npm:10.10.0" + tiny-warning: "npm:^1.0.2" + checksum: 10c0/d7505095cca00e9eee20563bd509fed35a4d9e293ad19820085f55bdba488141db75f430e7d87eed32c010e8b890636347bef9d18ad58cb19854c441b6b0537c + languageName: node + linkType: hard + +"jss-plugin-global@npm:10.10.0": + version: 10.10.0 + resolution: "jss-plugin-global@npm:10.10.0" + dependencies: + "@babel/runtime": "npm:^7.3.1" + jss: "npm:10.10.0" + checksum: 10c0/2d24ef0e16cd6ebcce59f132756716ae37fdffe3f59461018636a57ef68298e649f43bd5c346041f1642872aa2cc0629f5ecfb48a20bfb471813318cb8f3935f + languageName: node + linkType: hard + +"jss-plugin-nested@npm:10.10.0": + version: 10.10.0 + resolution: "jss-plugin-nested@npm:10.10.0" + dependencies: + "@babel/runtime": "npm:^7.3.1" + jss: "npm:10.10.0" + tiny-warning: "npm:^1.0.2" + checksum: 10c0/868ac4e4bea9dc02fac33f15e3165c008669d69e6b87201f1d8574eb213408b67366302288b49f46acda1320164460daa50e6aac817d34ae3b1c256a03f4ebba + languageName: node + linkType: hard + +"jss-plugin-props-sort@npm:10.10.0": + version: 10.10.0 + resolution: "jss-plugin-props-sort@npm:10.10.0" + dependencies: + "@babel/runtime": "npm:^7.3.1" + jss: "npm:10.10.0" + checksum: 10c0/5579bb21bfe514c12f43bd5e57458badc37c8e5676a47109f45195466a3aed633c61609daef079622421ef7c902b8342d1f96578543fefcb729f0b8dcfd2fe37 + languageName: node + linkType: hard + +"jss-plugin-rule-value-function@npm:10.10.0": + version: 10.10.0 + resolution: "jss-plugin-rule-value-function@npm:10.10.0" + dependencies: + "@babel/runtime": "npm:^7.3.1" + jss: "npm:10.10.0" + tiny-warning: "npm:^1.0.2" + checksum: 10c0/678bedb49da3b5e93fc1971d691f7f3ad2d7cf15dfc220edab934b70c7571fc383a435371a687a8ae125ab5ccd7bada9712574620959a3d1cd961fbca1583c29 + languageName: node + linkType: hard + +"jss-plugin-rule-value-observable@npm:10.10.0": + version: 10.10.0 + resolution: "jss-plugin-rule-value-observable@npm:10.10.0" + dependencies: + "@babel/runtime": "npm:^7.3.1" + jss: "npm:10.10.0" + symbol-observable: "npm:^1.2.0" + checksum: 10c0/1a8179a2567d39a75af1eecaeac89a99fbeb1515c4abfe04a73cba5eaa046e67a2c8d228204c660e0e3ccfd78f51e630f9face37bf54facc2c3b2bcd3e002426 + languageName: node + linkType: hard + +"jss-plugin-template@npm:10.10.0": + version: 10.10.0 + resolution: "jss-plugin-template@npm:10.10.0" + dependencies: + "@babel/runtime": "npm:^7.3.1" + jss: "npm:10.10.0" + tiny-warning: "npm:^1.0.2" + checksum: 10c0/2670a9cc31384b10d225218660a1566059cc1481d4a18f41199d0ad83190f3f0fd5f24ad33ed33c3c942cc8c5f2a36225e02e30b09f5e0ddc12bdbdde57bd28b + languageName: node + linkType: hard + +"jss-plugin-vendor-prefixer@npm:10.10.0": + version: 10.10.0 + resolution: "jss-plugin-vendor-prefixer@npm:10.10.0" + dependencies: + "@babel/runtime": "npm:^7.3.1" + css-vendor: "npm:^2.0.8" + jss: "npm:10.10.0" + checksum: 10c0/e3ad2dfe93d126f722586782aebddcd68dc46c0ad59f99edd65e164ecbb6e4cad6ce85c874f90553fa5fec50c2fd2b1f5984abfc4e3dd49d24033bbc378a2e11 + languageName: node + linkType: hard + +"jss-preset-default@npm:10.10.0, jss-preset-default@npm:^10.10.0": + version: 10.10.0 + resolution: "jss-preset-default@npm:10.10.0" + dependencies: + "@babel/runtime": "npm:^7.3.1" + jss: "npm:10.10.0" + jss-plugin-camel-case: "npm:10.10.0" + jss-plugin-compose: "npm:10.10.0" + jss-plugin-default-unit: "npm:10.10.0" + jss-plugin-expand: "npm:10.10.0" + jss-plugin-extend: "npm:10.10.0" + jss-plugin-global: "npm:10.10.0" + jss-plugin-nested: "npm:10.10.0" + jss-plugin-props-sort: "npm:10.10.0" + jss-plugin-rule-value-function: "npm:10.10.0" + jss-plugin-rule-value-observable: "npm:10.10.0" + jss-plugin-template: "npm:10.10.0" + jss-plugin-vendor-prefixer: "npm:10.10.0" + checksum: 10c0/a5781b10bf8e7171ba326b850e611e31192ea07220b975be2452ca0c7a87e39bc727cce6afa67c518ea769ff5f5565c0db989877cb99eeacac6c9d18b8e2ef8f + languageName: node + linkType: hard + +"jss@npm:10.10.0, jss@npm:^10.10.0": + version: 10.10.0 + resolution: "jss@npm:10.10.0" + dependencies: + "@babel/runtime": "npm:^7.3.1" + csstype: "npm:^3.0.2" + is-in-browser: "npm:^1.1.3" + tiny-warning: "npm:^1.0.2" + checksum: 10c0/aa5e743a3f40d6df05ae951c6913b6495ef42b3e9539f6875c32bf01c42ab405bd91038d6feca2ed5c67a2947111b0137213983089e2a310ee11fc563208ad61 + languageName: node + linkType: hard + "jsx-ast-utils@npm:^2.4.1 || ^3.0.0": version: 3.3.5 resolution: "jsx-ast-utils@npm:3.3.5" @@ -18030,7 +18334,7 @@ __metadata: languageName: node linkType: hard -"prop-types@npm:^15.6.1, prop-types@npm:^15.8.1": +"prop-types@npm:^15.5.8, prop-types@npm:^15.6.0, prop-types@npm:^15.6.1, prop-types@npm:^15.8.1": version: 15.8.1 resolution: "prop-types@npm:15.8.1" dependencies: @@ -18325,6 +18629,13 @@ __metadata: languageName: node linkType: hard +"react-display-name@npm:^0.2.4": + version: 0.2.5 + resolution: "react-display-name@npm:0.2.5" + checksum: 10c0/9c598283f2a545c01ba7fc81409b3fcd528d91925872e4033ffc51a9f675a3006acb9ec056877cc4fc0a9516cf7a769eb9c9e51dba6be1ff8d7dbd516dac397a + languageName: node + linkType: hard + "react-docgen@npm:5.3.1": version: 5.3.1 resolution: "react-docgen@npm:5.3.1" @@ -18396,6 +18707,27 @@ __metadata: languageName: node linkType: hard +"react-jss@npm:^10.10.0": + version: 10.10.0 + resolution: "react-jss@npm:10.10.0" + dependencies: + "@babel/runtime": "npm:^7.3.1" + "@emotion/is-prop-valid": "npm:^0.7.3" + css-jss: "npm:10.10.0" + hoist-non-react-statics: "npm:^3.2.0" + is-in-browser: "npm:^1.1.3" + jss: "npm:10.10.0" + jss-preset-default: "npm:10.10.0" + prop-types: "npm:^15.6.0" + shallow-equal: "npm:^1.2.0" + theming: "npm:^3.3.0" + tiny-warning: "npm:^1.0.2" + peerDependencies: + react: ">=16.8.6" + checksum: 10c0/38234c362fb21cac318e2be83840c8dd5a75a5689ee96d732997cc88c7775032d51d6186232a022c8b74c80c8d1ab943b0f984831a0e9c46357c75da919c66a6 + languageName: node + linkType: hard + "react-lifecycles-compat@npm:^3.0.4": version: 3.0.4 resolution: "react-lifecycles-compat@npm:3.0.4" @@ -19783,6 +20115,13 @@ __metadata: languageName: node linkType: hard +"shallow-equal@npm:^1.2.0": + version: 1.2.1 + resolution: "shallow-equal@npm:1.2.1" + checksum: 10c0/51e03abadd97c9ebe590547d92db9148446962a3f23a3a0fb1ba2fccab80af881eef0ff1f8ccefd3f066c0bc5a4c8ca53706194813b95c8835fa66448a843a26 + languageName: node + linkType: hard + "sharp@npm:0.32.6": version: 0.32.6 resolution: "sharp@npm:0.32.6" @@ -20848,6 +21187,13 @@ __metadata: languageName: node linkType: hard +"symbol-observable@npm:^1.2.0": + version: 1.2.0 + resolution: "symbol-observable@npm:1.2.0" + checksum: 10c0/009fee50798ef80ed4b8195048288f108b03de162db07493f2e1fd993b33fafa72d659e832b584da5a2427daa78e5a738fb2a9ab027ee9454252e0bedbcd1fdc + languageName: node + linkType: hard + "symbol-tree@npm:^3.2.4": version: 3.2.4 resolution: "symbol-tree@npm:3.2.4" @@ -21080,6 +21426,20 @@ __metadata: languageName: node linkType: hard +"theming@npm:^3.3.0": + version: 3.3.0 + resolution: "theming@npm:3.3.0" + dependencies: + hoist-non-react-statics: "npm:^3.3.0" + prop-types: "npm:^15.5.8" + react-display-name: "npm:^0.2.4" + tiny-warning: "npm:^1.0.2" + peerDependencies: + react: ">=16.3" + checksum: 10c0/15f0eaa3019cb77feb36837d06cb3c1641943e2e3fa06200ae6c996c1b5c7130a3442ddf513cb5723a1b95411287140d39b6bd6fa8ce61abce15eccd7c29c906 + languageName: node + linkType: hard + "thingies@npm:^1.20.0": version: 1.21.0 resolution: "thingies@npm:1.21.0" @@ -21127,6 +21487,13 @@ __metadata: languageName: node linkType: hard +"tiny-warning@npm:^1.0.2": + version: 1.0.3 + resolution: "tiny-warning@npm:1.0.3" + checksum: 10c0/ef8531f581b30342f29670cb41ca248001c6fd7975ce22122bd59b8d62b4fc84ad4207ee7faa95cde982fa3357cd8f4be650142abc22805538c3b1392d7084fa + languageName: node + linkType: hard + "title-case@npm:^3.0.3": version: 3.0.3 resolution: "title-case@npm:3.0.3"