-
Notifications
You must be signed in to change notification settings - Fork 11
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
base: main
Are you sure you want to change the base?
Changes from 7 commits
1a71958
55bba19
e55af0c
2dc1fda
fe6b9ba
6d83a8d
4a4c4d8
ea4caa0
05dc533
0c6cd0d
a6daccb
5a4592c
c78b142
c8945db
dc50b81
2bee8fd
e14fbfb
c22e049
1701b35
87a1049
2102b21
75dc134
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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' | ||
|
||
|
@@ -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') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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" | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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' | ||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 = () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 = () => ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
@@ -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> | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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' | ||
|
@@ -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]"> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
|
@@ -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" /> | ||
|
@@ -104,6 +131,24 @@ export function TopBar({ children }: { children: React.ReactNode }) { | |
</div> | ||
</div> | ||
</div> | ||
</> | ||
</div> | ||
) | ||
} | ||
|
||
const Menu12Icon = ({ className }: { className: string }) => ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
) |
There was a problem hiding this comment.
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.