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 7 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
142 changes: 136 additions & 6 deletions app/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,26 @@
*
* 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,
} 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,16 +67,113 @@ const JumpToButton = () => {
}

export function Sidebar({ children }: { children: React.ReactNode }) {
const AnimatedDialogContent = animated(Dialog.Content)
const { isOpen } = 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 },
})

return (
<div className="flex flex-col border-r text-sans-md text-default border-secondary">
<div className="mx-3 mt-4">
<JumpToButton />
</div>
{children}
<>
{transitions(
({ x }, item) =>
item && (
<Dialog.Root
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Feels slightly weird to be using a dialog here but it means we can use the same sidebar across both layouts. If there's a place that needs a bit more of a discerning eye it's probably here and the state logic to make sure it's bulletproof.

open
onOpenChange={(open) => {
if (!open) closeSidebar()
}}
// https://github.com/radix-ui/primitives/issues/1159#issuecomment-1559813266
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
>
<div className="mx-3 mt-4">
<JumpToButton />
</div>
{children}
{pathname.split('/')[1] !== 'settings' && <ProfileLinks />}
</AnimatedDialogContent>
</Dialog.Root>
)
)}
</>
)
}

export const ProfileLinks = () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Adds profile info into the navigation, hidden on desktop because it's visible in the nav.

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="lg+:hidden">
<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({})}>
<SignOut16Icon /> Sign out
</NavButtonItem>
</Sidebar.Nav>
</div>
)
}

const SignOut16Icon = () => (
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll move this into the design system

<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M7.25 2H3.25H2.75C2.33579 2 2 2.33579 2 2.75V3.25V12V13.25C2 13.6642 2.33579 14 2.75 14H4H7.25C7.66421 14 8 13.6642 8 13.25V12.75C8 12.3358 7.66421 12 7.25 12H4V4H7.25C7.66421 4 8 3.66421 8 3.25V2.75C8 2.33579 7.66421 2 7.25 2ZM13 7.75V8.25C13 8.66421 12.6642 9 12.25 9H7.75C7.33579 9 7 8.66421 7 8.25V7.75C7 7.33579 7.33579 7 7.75 7H12.25C12.6642 7 13 7.33579 13 7.75Z"
fill="currentColor"
/>
<rect
width="4"
height="5"
rx="0.75"
transform="matrix(4.37114e-08 -1 -1 -4.37114e-08 11 10)"
fill="currentColor"
/>
<path
d="M14.2679 8.58565C14.6432 8.2854 14.6432 7.71459 14.2679 7.41434L10.6093 4.48741C10.3637 4.29098 10 4.46579 10 4.78023L10 11.2198C10 11.5342 10.3637 11.709 10.6093 11.5126L14.2679 8.58565Z"
fill="currentColor"
/>
</svg>
)

interface SidebarNav {
children: React.ReactNode
heading?: string
Expand Down Expand Up @@ -109,3 +218,24 @@ export const NavLinkItem = (props: {
</li>
)
}

export const NavButtonItem = (props: {
onClick: () => void
children: React.ReactNode
disabled?: boolean
}) => (
<li>
<button
onClick={props.onClick}
className={cn(
linkStyles,
{
'pointer-events-none text-disabled': props.disabled,
},
'w-full'
)}
>
{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
71 changes: 58 additions & 13 deletions app/components/TopBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,19 @@
*
* 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 {
Close12Icon,
DirectionDownIcon,
Info16Icon,
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 +42,51 @@ 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"
title="Notifications"
onClick={(e) => {
if (isOpen) {
closeSidebar()
} else {
openSidebar()
}
e.preventDefault()
}}
>
{isOpen ? (
<Close12Icon className="text-tertiary" />
) : (
<Menu12Icon 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 Down Expand Up @@ -104,6 +131,24 @@ export function TopBar({ children }: { children: React.ReactNode }) {
</div>
</div>
</div>
</>
</div>
)
}

const Menu12Icon = ({ className }: { className: string }) => (
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Likewise, will also add to the design system

<svg
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1 1.667C1 1.29863 1.29863 1 1.667 1H10.333C10.7014 1 11 1.29863 11 1.667V2.333C11 2.70137 10.7014 3 10.333 3H1.667C1.29863 3 1 2.70137 1 2.333V1.667ZM1 5.667C1 5.29863 1.29863 5 1.667 5H10.333C10.7014 5 11 5.29863 11 5.667V6.333C11 6.70137 10.7014 7 10.333 7H1.667C1.29863 7 1 6.70137 1 6.333V5.667ZM11 9.667C11 9.29863 10.7014 9 10.333 9H1.667C1.29863 9 1 9.29863 1 9.667V10.333C1 10.7014 1.29863 11 1.667 11H10.333C10.7014 11 11 10.7014 11 10.333V9.667Z"
fill="currentColor"
/>
</svg>
)
Loading
Loading