Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/components/ErrorPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ type Props = { children: ReactNode; message?: string }

export function ErrorPage({ children }: Props) {
return (
<div className="flex w-full justify-center">
<div className="flex w-full justify-center pt-(--preview-banner-height)">
<GradientBackground />
<div className="relative flex w-full justify-between">
<Link
Expand Down
43 changes: 24 additions & 19 deletions app/components/MswBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*
* Copyright Oxide Computer Company
*/
import cn from 'classnames'
import { useState, type ReactNode } from 'react'

import { Info16Icon, NextArrow12Icon } from '@oxide/design-system/icons/react'
Expand All @@ -26,36 +27,40 @@ function ExternalLink({ href, children }: { href: string; children: ReactNode })
)
}

type Props = {
/**
* HACK to avoid the user opening the modal while on the loading skeleton
* -- it immediately closes when the page finishes loading because the
* banner is dropped when the HydrateFallback unmounts and re-rendered in
* RootLayout. A more ideal solution would be to render the banner outside
* the RouterProvider and therefore have it be the same banner in both the
* HydrateFallback and normal page situations, but it's a lot more work to
* get the layout right in that case with respect to things like the loading
* bar. When we switch to framework mode, we can manage all this in the root
* route using the Layout export. In the meantime, this is tolerable and only
* applies to the preview deploys, and only burdens someone who manages to
* click the Learn More button in the half second before the content loads.
*/
disableButton?: boolean
/**
* Renders the preview banner (when enabled at build time) and sets
* `--preview-banner-height` for the rest of the app. The banner is `fixed`, so
* it can't push anything down through normal flow. Instead, the fixed-position
* chrome (top bar, sidebar) and viewport-height calcs consume the variable to
* offset themselves, the same way they use `--top-bar-height`. When the banner
* is off, the variable stays at its 0px default (set in index.css) and the
* offsets collapse to nothing, so consumers don't need their own conditionals.
*
* Rendered once in main.tsx, outside the router, so the same banner instance
* persists across hydration (skeleton to real page) and error states.
*/
export function PreviewBannerLayout({ children }: { children: ReactNode }) {
return (
<div
className={cn('h-full', process.env.MSW_BANNER && '[--preview-banner-height:2.5rem]')}
>
{process.env.MSW_BANNER ? <MswBanner /> : null}
{children}
</div>
)
}

export function MswBanner({ disableButton }: Props) {
function MswBanner() {
const [isOpen, setIsOpen] = useState(false)
const closeModal = () => setIsOpen(false)
return (
<>
{/* The [&+*]:pt-10 style is to ensure the page container isn't pushed out of screen as it uses 100vh for layout */}
<aside className="text-sans-md text-info bg-info absolute z-(--z-top-bar) flex h-10 w-full items-center justify-center [&+*]:pt-10">
<aside className="text-sans-md text-info bg-info fixed top-0 z-(--z-top-bar) flex h-(--preview-banner-height) w-full items-center justify-center">
<Info16Icon className="mr-2" /> This is a technical preview.
<button
type="button"
className="text-sans-md hover:text-info ml-2 flex items-center gap-0.5"
onClick={() => setIsOpen(true)}
disabled={disableButton}
>
Learn more <NextArrow12Icon />
</button>
Expand Down
64 changes: 29 additions & 35 deletions app/components/PageSkeleton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ import {
} from '~/layouts/helpers'
import { classed } from '~/util/classed'

import { MswBanner } from './MswBanner'

const Block = classed.div`motion-safe:animate-pulse2 rounded-md bg-tertiary`

export function PageSkeleton({ skipPaths }: { skipPaths?: RegExp[] }) {
Expand All @@ -29,42 +27,38 @@ export function PageSkeleton({ skipPaths }: { skipPaths?: RegExp[] }) {
// layout, all we can do is match the path
if (skipPaths?.some((regex) => regex.test(pathname))) return null

// we need the msw banner here so it doesn't pop in on load
return (
<>
{process.env.MSW_BANNER ? <MswBanner disableButton /> : null}
<PageContainer>
{/* TopBar */}
<div className={topBarWrapperClass}>
<div className="border-secondary flex items-center gap-2 border-r p-3">
<Block className="h-8 w-8" />
<Block className="h-4 w-24" />
</div>
<div className="flex items-center justify-between gap-2 p-3">
<Block className="h-4 w-24" />
<div className="flex items-center gap-2">
<Block className="h-6 w-16" />
<Block className="h-6 w-32" />
</div>
</div>
<PageContainer>
{/* TopBar */}
<div className={topBarWrapperClass}>
<div className="border-secondary flex items-center gap-2 border-r p-3">
<Block className="h-8 w-8" />
<Block className="h-4 w-24" />
</div>
{/* Sidebar */}
<div className={cn(sidebarWrapperClass, 'p-4')}>
<Block className="mb-10 h-4 w-full" />
<div className="mb-6 space-y-2">
<Block className="h-4 w-32" />
<Block className="h-4 w-24" />
</div>
<div className="space-y-2">
<Block className="h-4 w-14" />
<Block className="h-4 w-32" />
<Block className="h-4 w-24" />
<Block className="h-4 w-14" />
<div className="flex items-center justify-between gap-2 p-3">
<Block className="h-4 w-24" />
<div className="flex items-center gap-2">
<Block className="h-6 w-16" />
<Block className="h-6 w-32" />
</div>
</div>
{/* Content */}
<ContentPane />
</PageContainer>
</>
</div>
{/* Sidebar */}
<div className={cn(sidebarWrapperClass, 'p-4')}>
<Block className="mb-10 h-4 w-full" />
<div className="mb-6 space-y-2">
<Block className="h-4 w-32" />
<Block className="h-4 w-24" />
</div>
<div className="space-y-2">
<Block className="h-4 w-14" />
<Block className="h-4 w-32" />
<Block className="h-4 w-24" />
<Block className="h-4 w-14" />
</div>
</div>
{/* Content */}
<ContentPane />
</PageContainer>
)
}
2 changes: 1 addition & 1 deletion app/layouts/AuthLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { OxideLogo } from '~/components/OxideLogo'
export default function AuthLayout() {
return (
<main
className="relative h-screen"
className="relative h-screen pt-(--preview-banner-height)"
style={{
background:
'radial-gradient(200% 100% at 50% 100%, var(--surface-default) 0%, var(--surface-secondary) 100%)',
Expand Down
2 changes: 1 addition & 1 deletion app/layouts/LoginLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { OxideLogo } from '~/components/OxideLogo'

export default function LoginLayout() {
return (
<main className="layout relative flex h-screen">
<main className="layout relative flex h-screen pt-(--preview-banner-height)">
<div className="hero-bg text-accent max-800:hidden relative flex w-1/2 justify-end">
<div className="hero-rack-wrapper">
<img src={heroRackImg} alt="A populated Oxide rack" className="hero-rack" />
Expand Down
2 changes: 0 additions & 2 deletions app/layouts/RootLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import { useEffect, useRef } from 'react'
import { Outlet, useNavigation } from 'react-router'

import { MswBanner } from '~/components/MswBanner'
import { ToastStack } from '~/components/ToastStack'
import { useCrumbs } from '~/hooks/use-crumbs'
import { useApplyTheme } from '~/stores/theme'
Expand Down Expand Up @@ -38,7 +37,6 @@ export default function RootLayout() {
return (
<>
<LoadingBar />
{process.env.MSW_BANNER ? <MswBanner /> : null}
<Outlet />
<ToastStack />
</>
Expand Down
10 changes: 5 additions & 5 deletions app/layouts/helpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,18 @@ import { useScrollRestoration } from '~/hooks/use-scroll-restoration'
import { SkipLinkTarget } from '~/ui/lib/SkipLink'
import { classed } from '~/util/classed'

export const PageContainer = classed.div`min-h-full pt-(--top-bar-height)`
export const PageContainer = classed.div`min-h-full pt-[calc(var(--top-bar-height)+var(--preview-banner-height))]`

// shared with PageSkeleton so the skeleton doesn't drift from the real layout
export const topBarWrapperClass =
'bg-default border-secondary fixed top-0 right-0 left-0 z-(--z-top-bar) grid h-(--top-bar-height) grid-cols-[var(--sidebar-width)_1fr] border-b'
'bg-default border-secondary fixed top-(--preview-banner-height) right-0 left-0 z-(--z-top-bar) grid h-(--top-bar-height) grid-cols-[var(--sidebar-width)_1fr] border-b'
export const sidebarWrapperClass =
'border-secondary fixed top-(--top-bar-height) bottom-0 left-0 w-(--sidebar-width) border-r'
'border-secondary fixed top-[calc(var(--top-bar-height)+var(--preview-banner-height))] bottom-0 left-0 w-(--sidebar-width) border-r'

export function ContentPane() {
useScrollRestoration()
return (
<div className="light:bg-raise ml-(--sidebar-width) flex min-h-[calc(100vh-var(--top-bar-height))] flex-col">
<div className="light:bg-raise ml-(--sidebar-width) flex min-h-[calc(100vh-var(--top-bar-height)-var(--preview-banner-height))] flex-col">
<div className="flex grow flex-col pb-8">
<SkipLinkTarget />
<main className="*:gutter">
Expand All @@ -46,7 +46,7 @@ export function ContentPane() {
* `<div>` because we don't need it.
*/
export const SerialConsoleContentPane = () => (
<div className="ml-(--sidebar-width) flex h-[calc(100vh-var(--top-bar-height))] flex-col overflow-hidden">
<div className="ml-(--sidebar-width) flex h-[calc(100vh-var(--top-bar-height)-var(--preview-banner-height))] flex-col overflow-hidden">
<SkipLinkTarget />
<main className="*:gutter h-full">
<Outlet />
Expand Down
13 changes: 8 additions & 5 deletions app/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { queryClient } from '@oxide/api'

import { ConfirmActionModal } from './components/ConfirmActionModal'
import { ErrorBoundary } from './components/ErrorBoundary'
import { PreviewBannerLayout } from './components/MswBanner'
// stripped out by rollup in production
import { startMockAPI } from './msw-mock-api'
import { routes } from './routes'
Expand Down Expand Up @@ -51,11 +52,13 @@ function render() {
<QueryClientProvider client={queryClient}>
<LazyMotion strict features={loadFeatures}>
<MotionConfig reducedMotion="user">
<ErrorBoundary>
<ConfirmActionModal />
<SkipLink id="skip-nav" />
<RouterProvider router={router} />
</ErrorBoundary>
<PreviewBannerLayout>
<ErrorBoundary>
<ConfirmActionModal />
<SkipLink id="skip-nav" />
<RouterProvider router={router} />
</ErrorBoundary>
</PreviewBannerLayout>
</MotionConfig>
</LazyMotion>
{/* <ReactQueryDevtools initialIsOpen={false} /> */}
Expand Down
2 changes: 1 addition & 1 deletion app/pages/project/instances/SerialConsolePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ export default function SerialConsolePage() {
}, [canConnect])

return (
<div className="mx-0! flex h-full max-h-[calc(100vh-var(--top-bar-height))] w-full! flex-col">
<div className="mx-0! flex h-full max-h-[calc(100vh-var(--top-bar-height)-var(--preview-banner-height))] w-full! flex-col">
<Link
to={pb.instance(instanceSelector)}
className="bg-accent mx-3 mt-3 mb-6 flex h-10 shrink-0 items-center rounded-md px-3"
Expand Down
2 changes: 2 additions & 0 deletions app/ui/styles/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@
--content-gutter: 2.5rem;
--top-bar-height: 54px;
--sidebar-width: 14.25rem;
/* overridden by PreviewBannerLayout when the banner is enabled */
--preview-banner-height: 0px;

@media (max-width: 767px) {
--content-gutter: 1.5rem;
Expand Down
Loading