Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mobile and tablet layouts #2087

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 18 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
7 changes: 6 additions & 1 deletion app/components/EquivalentCliCommand.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@ export function EquivalentCliCommand({ command }: { command: string }) {

return (
<>
<Button variant="ghost" size="sm" className="ml-2" onClick={() => setIsOpen(true)}>
<Button
variant="ghost"
size="sm"
className="md-:hidden"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be disabled universally I think. Users will not be using the CLI from their phones.

onClick={() => setIsOpen(true)}
>
Equivalent CLI Command
</Button>
<Modal isOpen={isOpen} onDismiss={handleDismiss} title="CLI command">
Expand Down
4 changes: 2 additions & 2 deletions app/components/ErrorPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ export function ErrorPage({ children }: Props) {
to="/"
className="flex items-center p-6 text-mono-sm text-secondary hover:text-default"
>
<PrevArrow12Icon title="Select" className="mr-2 w-2 text-tertiary" />
<PrevArrow12Icon title="Select" className="mr-2 text-tertiary" />
Back to console
</Link>
</div>
<div className="absolute left-1/2 top-1/2 flex w-96 -translate-x-1/2 -translate-y-1/2 flex-col items-center justify-center space-y-4 rounded-lg border p-8 !bg-raise border-secondary elevation-3">
<div className="absolute left-1/2 top-1/2 flex w-96 -translate-x-1/2 -translate-y-1/2 flex-col items-center justify-center space-y-4 rounded-lg border p-8 !bg-raise border-secondary elevation-3 md-:w-[calc(100%-(var(--content-gutter)*2))]">
<div className="my-2 flex h-12 w-12 items-center justify-center">
<div className="absolute h-12 w-12 rounded-full opacity-20 bg-destructive motion-safe:animate-[ping_2s_cubic-bezier(0,0,0.2,1)_infinite]" />
<Error12Icon className="relative h-8 w-8 text-error" />
Expand Down
14 changes: 11 additions & 3 deletions app/components/MswBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*
* Copyright Oxide Computer Company
*/
import { useState, type ReactNode } from 'react'
import { useEffect, useState, type ReactNode } from 'react'

import { Info16Icon, NextArrow12Icon } from '@oxide/design-system/icons/react'

Expand All @@ -29,10 +29,18 @@ function ExternalLink({ href, children }: { href: string; children: ReactNode })
export function MswBanner() {
const [isOpen, setIsOpen] = useState(false)
const closeModal = () => setIsOpen(false)

useEffect(() => {
document.body.classList.add('msw-banner')
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps convoluted but only appears on the preview so not too concerned about it. We use it to add extra padding for the banner at the bottom. Banner has been moved to the bottom, mostly because it breaks less stuff in the navigation.


return () => {
document.body.classList.remove('msw-banner')
}
}, [])

return (
<>
{/* The [&+*]:pt-10 style is to ensure the page container isn't pushed out of screen as it uses 100vh for layout */}
<label className="absolute z-topBar flex h-10 w-full items-center justify-center text-sans-md text-info-secondary bg-info-secondary [&+*]:pt-10">
<label className="fixed bottom-0 z-topBar flex h-10 w-full items-center justify-center text-sans-md text-info-secondary bg-info-secondary [&+*]:pt-[calc(--navigation-height)]">
<Info16Icon className="mr-2" /> This is a technical preview.
<button
className="ml-2 flex items-center gap-0.5 text-sans-md hover:text-info"
Expand Down
4 changes: 2 additions & 2 deletions app/components/RefetchIntervalPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export function useIntervalPicker({ enabled, isLoading, fn }: Props) {
intervalMs: (enabled && intervalPresets[intervalPreset]) || undefined,
intervalPicker: (
<div className="mb-12 flex items-center justify-between">
<div className="hidden items-center gap-2 text-right text-mono-sm text-quaternary lg+:flex">
<div className="flex items-center gap-2 text-right text-mono-sm text-quaternary">
<Time16Icon className="text-quinary" /> Refreshed {format(lastFetched, 'HH:mm')}
</div>
<div className="flex">
Expand All @@ -73,7 +73,7 @@ export function useIntervalPicker({ enabled, isLoading, fn }: Props) {
</button>
<Listbox
selected={enabled ? intervalPreset : 'Off'}
className="w-24 [&>button]:!rounded-l-none"
className="w-24 md-:w-full [&>button]:!rounded-l-none"
items={intervalItems}
onChange={setIntervalPreset}
disabled={!enabled}
Expand Down
134 changes: 131 additions & 3 deletions app/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,27 @@
*
* Copyright Oxide Computer Company
*/
import * as Dialog from '@radix-ui/react-dialog'
import { animated, useTransition } from '@react-spring/web'
import cn from 'classnames'
import { NavLink, useLocation } from 'react-router-dom'

import { Action16Icon, Document16Icon } from '@oxide/design-system/icons/react'
import {
Action16Icon,
Document16Icon,
Key16Icon,
Profile16Icon,
SignOut16Icon,
} from '@oxide/design-system/icons/react'

import { navToLogin, useApiMutation } from '~/api'
import { openQuickActions } from '~/hooks'
import { closeSidebar, useMenuState } from '~/hooks/use-menu-state'
import { useCurrentUser } from '~/layouts/AuthenticatedLayout'
import { Button } from '~/ui/lib/Button'
import { Divider } from '~/ui/lib/Divider'
import { Truncate } from '~/ui/lib/Truncate'
import { pb } from '~/util/path-builder'

const linkStyles =
'flex h-7 items-center rounded px-2 text-sans-md hover:bg-hover svg:mr-2 svg:text-quinary text-secondary'
Expand Down Expand Up @@ -55,12 +68,104 @@ const JumpToButton = () => {
}

export function Sidebar({ children }: { children: React.ReactNode }) {
return (
<div className="flex flex-col border-r text-sans-md text-default border-secondary">
const AnimatedDialogContent = animated(Dialog.Content)
const { isOpen, isSmallScreen } = useMenuState()
const config = { tension: 1200, mass: 0.125 }
const { pathname } = useLocation()

const transitions = useTransition(isOpen, {
from: { x: -50 },
enter: { x: 0 },
config: isOpen ? config : { duration: 0 },
})

const SidebarContent = () => (
<>
<div className="mx-3 mt-4">
<JumpToButton />
</div>
{children}
{pathname.split('/')[1] !== 'settings' && <ProfileLinks className="lg+:hidden" />}
</>
)

if (isSmallScreen) {
return (
<>
{transitions(
({ x }, item) =>
item && (
<Dialog.Root
open
onOpenChange={(open) => {
if (!open) closeSidebar()
}}
// Modal needs to be false to be able to click on top bar
modal={false}
>
<div
aria-hidden
className="fixed inset-0 top-[61px] z-10 overflow-auto bg-scrim lg+:hidden"
/>
<AnimatedDialogContent
className="fixed z-sideModal flex h-full w-[14.25rem] flex-col border-r text-sans-md text-default border-secondary lg+:!transform-none lg-:inset-y-0 lg-:top-[61px] lg-:bg-default lg-:elevation-2"
style={{
transform: x.to((value) => `translate3d(${value}%, 0px, 0px)`),
}}
forceMount
onInteractOutside={(e) => {
// We want to handle opening / closing with the menu button ourselves
// Not doing this can result in the two events fighting
if ((e.target as HTMLElement)?.title === 'Sidebar') {
e.preventDefault()
e.stopPropagation()
return null
}
}}
>
<SidebarContent />
</AnimatedDialogContent>
</Dialog.Root>
)
)}
</>
)
} else {
return (
<div className="fixed z-sideModal flex h-full w-[14.25rem] flex-col border-r text-sans-md text-default border-secondary lg+:!transform-none lg-:inset-y-0 lg-:top-[61px] lg-:bg-default lg-:elevation-2">
<SidebarContent />
</div>
)
}
}

export const ProfileLinks = ({ className }: { className?: string }) => {
const { me } = useCurrentUser()

const logout = useApiMutation('logout', {
onSuccess: () => {
// server will respond to /login with a login redirect
// TODO-usability: do we just want to dump them back to login or is there
// another page that would make sense, like a logged out homepage
navToLogin({ includeCurrent: false })
},
})

return (
<div className={cn(className, '')}>
<Divider />
<Sidebar.Nav heading={me.displayName || 'User'}>
<NavLinkItem to={pb.profile()}>
<Profile16Icon />
Profile
</NavLinkItem>
<NavLinkItem to={pb.sshKeys()}>
<Key16Icon /> SSH Keys
</NavLinkItem>
<NavButtonItem onClick={() => logout.mutate({})} className="lg+:hidden">
<SignOut16Icon /> Sign Out
</NavButtonItem>
</Sidebar.Nav>
</div>
)
}
Expand Down Expand Up @@ -109,3 +214,26 @@ export const NavLinkItem = (props: {
</li>
)
}

export const NavButtonItem = (props: {
onClick: () => void
children: React.ReactNode
disabled?: boolean
className?: string
}) => (
<li>
<button
onClick={props.onClick}
className={cn(
linkStyles,
{
'pointer-events-none text-disabled': props.disabled,
},
'w-full',
props.className
)}
>
{props.children}
</button>
</li>
)
7 changes: 5 additions & 2 deletions app/components/Terminal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,11 @@ export default function Terminal({ ws }: TerminalProps) {

return (
<>
<div className="h-full w-[calc(100%-3rem)] text-mono-code" ref={terminalRef} />
<div className="absolute right-0 top-0 space-y-2 text-secondary">
<div
className="h-full w-full text-mono-code md+:w-[calc(100%-3rem)]"
ref={terminalRef}
/>
<div className="absolute right-0 top-0 space-y-2 text-secondary md-:hidden">
<ScrollButton onClick={() => term?.scrollToTop()}>
<DirectionUpIcon />
</ScrollButton>
Expand Down
55 changes: 41 additions & 14 deletions app/components/TopBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,20 @@
*
* Copyright Oxide Computer Company
*/
import cn from 'classnames'
import React from 'react'
import { useNavigate } from 'react-router-dom'

import { navToLogin, useApiMutation } from '@oxide/api'
import {
DirectionDownIcon,
Info16Icon,
MenuClose12Icon,
MenuOpen12Icon,
Profile16Icon,
} from '@oxide/design-system/icons/react'

import { closeSidebar, openSidebar, useMenuState } from '~/hooks/use-menu-state'
import { useCurrentUser } from '~/layouts/AuthenticatedLayout'
import { Button, buttonStyle } from '~/ui/lib/Button'
import { DropdownMenu } from '~/ui/lib/DropdownMenu'
Expand All @@ -39,27 +43,50 @@ export function TopBar({ children }: { children: React.ReactNode }) {
// picker is going to come in null when the user isn't supposed to see it
const [cornerPicker, ...otherPickers] = React.Children.toArray(children)

// The height of this component is governed by the `PageContainer`
// It's important that this component returns two distinct elements (wrapped in a fragment).
// Each element will occupy one of the top column slots provided by `PageContainer`.
const { isOpen } = useMenuState()

return (
<>
<div className="flex items-center border-b border-r px-3 border-secondary">
<div className="fixed top-0 z-topBar col-span-2 grid h-[var(--navigation-height)] w-full grid-cols-[min-content,auto] bg-default lg+:grid-cols-[var(--sidebar-width),auto]">
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this stuff freaks me out. wonder if we can drop the grid cols thing

<div className="flex items-center border-b pl-3 border-secondary lg+:border-r lg+:pr-3">
<Button
variant="ghost"
size="icon"
className="mr-2 w-8 flex-shrink-0 lg+:hidden [&>*]:pointer-events-none"
title="Sidebar"
onClick={() => {
if (isOpen) {
closeSidebar()
} else {
openSidebar()
}
}}
>
{isOpen ? (
<MenuClose12Icon className="text-tertiary" />
) : (
<MenuOpen12Icon className="text-tertiary" />
)}
</Button>

{cornerPicker}
</div>
{/* Height is governed by PageContainer grid */}
{/* shrink-0 is needed to prevent getting squished by body content */}
<div className="z-topBar border-b bg-default border-secondary">
<div className="mx-3 flex h-[60px] shrink-0 items-center justify-between">
<div className="flex items-center">{otherPickers}</div>

<div className="border-b bg-default border-secondary">
<div className="mr-3 flex h-[var(--navigation-height)] shrink-0 items-center justify-between lg+:ml-3">
<div className="pickers before:text-mono-lg flex items-center before:children:content-['/'] before:children:first:mx-3 before:children:first:text-quinary md-:children:hidden lg+:[&>div:first-of-type]:before:hidden md-:[&>div:last-of-type]:flex">
{otherPickers}
</div>
<div>
<a
id="topbar-info-link"
href="https://docs.oxide.computer/guides"
target="_blank"
rel="noreferrer"
aria-label="Link to documentation"
className={buttonStyle({ size: 'icon', variant: 'secondary' })}
className={cn(
buttonStyle({ size: 'icon', variant: 'secondary' }),
'md-:hidden'
)}
>
<Info16Icon className="text-quaternary" />
</a>
Expand All @@ -72,7 +99,7 @@ export function TopBar({ children }: { children: React.ReactNode }) {
size="sm"
variant="secondary"
aria-label="User menu"
className="ml-2"
className="ml-2 md-:hidden"
innerClassName="space-x-2"
>
<Profile16Icon className="text-quaternary" />
Expand All @@ -92,7 +119,7 @@ export function TopBar({ children }: { children: React.ReactNode }) {
</DropdownMenu.Item>
{loggedIn ? (
<DropdownMenu.Item onSelect={() => logout.mutate({})}>
Sign out
Sign Out
</DropdownMenu.Item>
) : (
<DropdownMenu.Item onSelect={() => navToLogin({ includeCurrent: true })}>
Expand All @@ -104,6 +131,6 @@ export function TopBar({ children }: { children: React.ReactNode }) {
</div>
</div>
</div>
</>
</div>
)
}
Loading
Loading