diff --git a/apps/app-frontend/index.html b/apps/app-frontend/index.html index 5239fe4c..2e4e75ad 100644 --- a/apps/app-frontend/index.html +++ b/apps/app-frontend/index.html @@ -5,6 +5,8 @@ OSAC + +
diff --git a/apps/app-frontend/package.json b/apps/app-frontend/package.json index e74e48da..088d5c06 100644 --- a/apps/app-frontend/package.json +++ b/apps/app-frontend/package.json @@ -15,11 +15,11 @@ "@connectrpc/connect-web": "^2.1.2", "@osac/types": "workspace:*", "@osac/ui-components": "workspace:*", - "@patternfly/patternfly": "^6.4.0", - "@patternfly/react-charts": "^8.0.0", - "@patternfly/react-core": "^6.4.1", - "@patternfly/react-icons": "^6.4.0", - "@patternfly/react-table": "^6.4.0", + "@patternfly/patternfly": "^6.6.1", + "@patternfly/react-charts": "^8.6.1", + "@patternfly/react-core": "^6.6.1", + "@patternfly/react-icons": "^6.6.1", + "@patternfly/react-table": "^6.6.1", "@tanstack/react-query": "^5.0.0", "i18next": "^23.0.0", "i18next-browser-languagedetector": "^8.0.0", @@ -28,6 +28,7 @@ "react-dom": "^19.2.4", "react-i18next": "^15.0.0", "react-router-dom": "^7.0.0", + "react-svg": "^16.2.0", "victory": "^37.3.6" }, "devDependencies": { diff --git a/apps/app-frontend/public/theme-boot.js b/apps/app-frontend/public/theme-boot.js new file mode 100644 index 00000000..0785b455 --- /dev/null +++ b/apps/app-frontend/public/theme-boot.js @@ -0,0 +1,19 @@ +// Apply PatternFly theme/contrast before React boots to avoid a flash of the wrong theme. +(function () { + const html = document.documentElement; + // Felt brand tokens — always on + html.classList.add('pf-v6-theme-felt'); + + let theme = localStorage.getItem('osac/theme') || 'system'; + if (theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches) { + theme = 'dark'; + } + html.classList.toggle('pf-v6-theme-dark', theme === 'dark'); + + let contrast = localStorage.getItem('osac/contrast') || 'system'; + if (contrast === 'system') { + contrast = window.matchMedia('(prefers-contrast: more)').matches ? 'contrast' : 'glass'; + } + html.classList.toggle('pf-v6-theme-high-contrast', contrast === 'contrast'); + html.classList.toggle('pf-v6-theme-glass', contrast === 'glass'); +})(); diff --git a/apps/app-frontend/src/shell/AppShell.tsx b/apps/app-frontend/src/shell/AppShell.tsx index 541029b5..3bdad4ed 100644 --- a/apps/app-frontend/src/shell/AppShell.tsx +++ b/apps/app-frontend/src/shell/AppShell.tsx @@ -1,13 +1,15 @@ import { type ReactNode } from 'react'; import { Navigate, Route, Routes, useLocation } from 'react-router-dom'; -import { Page } from '@patternfly/react-core'; +import { Page, SkipToContent } from '@patternfly/react-core'; +import { CatalogItemDetailPage } from '@osac/ui-components/components/catalog/details/CatalogItemDetailPage.tsx'; import ErrorBoundary from '@osac/ui-components/components/ErrorBoundary/ErrorBoundary'; import IdentityProviderRoutes from '@osac/ui-components/components/IdentityProvider/IdentityProviderRoutes'; import ProjectRoutes from '@osac/ui-components/components/Project/ProjectRoutes'; import RoleBindingRoutes from '@osac/ui-components/components/RoleBinding/RoleBindingRoutes'; import { VmDetailsPage } from '@osac/ui-components/components/vm/VmDetailsPage'; import { useSession } from '@osac/ui-components/hooks/use-session'; +import { useTranslation } from '@osac/ui-components/hooks/useTranslation'; import { SecurityGroupDetailPage } from '@osac/ui-components/pages/networking/SecurityGroupDetailPage'; import { SecurityGroupsListPage } from '@osac/ui-components/pages/networking/SecurityGroupsListPage'; import { VirtualNetworkDetailPage } from '@osac/ui-components/pages/networking/VirtualNetworkDetailPage'; @@ -25,6 +27,8 @@ import { ShellSidebar } from './ShellSidebar'; import { StorageRoutes } from './StorageRoutes'; import { TenantRoutes } from './TenantRoutes'; +const MAIN_CONTENT_ID = 'osac-main-content'; + const ShellRoute = ({ children }: { children: ReactNode }) => { const { pathname } = useLocation(); @@ -33,6 +37,7 @@ const ShellRoute = ({ children }: { children: ReactNode }) => { export const AppShell = ({ logout }: { logout: () => Promise }) => { const { role } = useSession(); + const { t } = useTranslation(); const defaultRoute = defaultRouteForRole(role); @@ -41,6 +46,11 @@ export const AppShell = ({ logout }: { logout: () => Promise }) => { masthead={} sidebar={} isManagedSidebar + mainContainerId={MAIN_CONTENT_ID} + skipToContent={ + {t('Skip to content')} + } + isContentFilled > Promise }) => { } /> + + + + } + /> { const [logoutError, setLogoutError] = React.useState(); const navigate = useNavigate(); const { role, username, tenantId } = useSession(); - const displayName = username || 'User'; + const displayName = username || t('User'); return ( <> {logoutError && ( setLogoutError(undefined)}> - + @@ -68,22 +67,23 @@ export const ShellMasthead = ({ onLogout }: ShellMastheadProps) => { - - - + - +
- - - Red Hat OSAC - - {tenantId && ( - {t('Tenant: {{ tenantId }}', { tenantId })} - )} - + } + > + + - + {tenantId ? ( + + {t('Tenant: {{ tenantId }}', { tenantId })} + + ) : null} +
@@ -106,7 +106,7 @@ export const ShellMasthead = ({ onLogout }: ShellMastheadProps) => { isExpanded={isUserMenuOpen} onClick={() => setIsUserMenuOpen(!isUserMenuOpen)} icon={} - aria-label="Account menu" + aria-label={t('Account menu')} > {displayName}{' '}