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 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
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
2 changes: 1 addition & 1 deletion app/components/ErrorPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export function ErrorPage({ children }: Props) {
</Link>
<SignOutButton className="mr-6 mt-4" />
</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
type="button"
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{' '}
{toLocaleTimeString(lastFetched)}
</div>
Expand All @@ -75,7 +75,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
133 changes: 130 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 { closeSidebar, useMenuState } from '~/hooks/use-menu-state'
import { openQuickActions } from '~/hooks/use-quick-actions'
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,25 @@ export const NavLinkItem = (props: {
</li>
)
}

export const NavButtonItem = (props: {
onClick: () => void
children: React.ReactNode
disabled?: boolean
className?: string
}) => (
<li>
<button
type="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()} aria-label="Scroll to top">
<DirectionUpIcon aria-hidden />
</ScrollButton>
Expand Down
70 changes: 56 additions & 14 deletions app/components/TopBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,21 @@
*
* Copyright Oxide Computer Company
*/
import cn from 'classnames'
import React from 'react'

import { navToLogin, useApiMutation } from '@oxide/api'
import { DirectionDownIcon, Profile16Icon } from '@oxide/design-system/icons/react'
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 } from '~/ui/lib/Button'
import { Button, buttonStyle } from '~/ui/lib/Button'
import { DropdownMenu } from '~/ui/lib/DropdownMenu'
import { pb } from '~/util/path-builder'

Expand All @@ -26,20 +34,53 @@ 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 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="flex items-center gap-2">

<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={cn(
buttonStyle({ size: 'icon', variant: 'secondary' }),
'md-:hidden'
)}
>
<Info16Icon className="text-quaternary" />
</a>
{/* <Button variant="secondary" size="icon" className="ml-2" title="Notifications">
<Notifications16Icon className="text-quaternary" />
</Button> */}
Expand All @@ -49,6 +90,7 @@ export function TopBar({ children }: { children: React.ReactNode }) {
size="sm"
variant="secondary"
aria-label="User menu"
className="ml-2 md-:hidden"
innerClassName="space-x-2"
>
<Profile16Icon className="text-quaternary" />
Expand All @@ -68,6 +110,6 @@ export function TopBar({ children }: { children: React.ReactNode }) {
</div>
</div>
</div>
</>
</div>
)
}
16 changes: 11 additions & 5 deletions app/components/TopBarPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const TopBarPicker = (props: TopBarPickerProps) => {
// left corner picker. The separator starts from the leftmost of "other
// pickers". But the leftmost corner one is in its own container and
// therefore always last-of-type, so it will never get one.
className="after:text-mono-lg flex w-full items-center justify-between after:mx-4 after:content-['/'] after:text-quinary last-of-type:after:content-none"
className="flex w-full items-center justify-between"
>
{props.current ? (
<Wrap
Expand All @@ -65,11 +65,17 @@ const TopBarPicker = (props: TopBarPickerProps) => {
<Link to={props.to!} className="-m-1 grow rounded-lg p-1 hover:bg-hover" />
}
>
<div className="flex min-w-[120px] max-w-[185px] items-center pr-2">
<div
className={cn(
'flex max-w-[185px] items-center pr-2 lg+:min-w-[120px]',
props.icon && 'md-:pr-0'
)}
>
{props.icon ? (
<div className="mr-2 flex items-center">{props.icon}</div>
<div className="flex items-center md+:mr-2">{props.icon}</div>
) : null}
<div className="overflow-hidden">
{/* If it has an icon it must be a silo picker and has specific styling on mobile */}
<div className={cn('overflow-hidden', props.icon && 'md-:hidden')}>
<div className="text-mono-xs text-quaternary">{props.category}</div>
<div className="w-full overflow-hidden text-ellipsis whitespace-nowrap text-sans-md text-secondary">
{props.display ?? props.current}
Expand All @@ -84,7 +90,7 @@ const TopBarPicker = (props: TopBarPickerProps) => {
>
{props.icon ? <div className="mr-2 flex items-center">{props.icon}</div> : null}

<div className="min-w-[5rem] text-mono-xs text-quaternary">
<div className="text-mono-xs text-quaternary lg+:min-w-[5rem] lg-:mr-1">
Select
<br />
{props.category}
Expand Down
Loading