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

impl: sidebar defaults to open on large screens #105

Open
wants to merge 5 commits into
base: master
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
74 changes: 49 additions & 25 deletions src/components/material/Drawer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { JSXElement, ParentComponent } from 'solid-js'
import type { JSXElement, JSX, Component, ParentComponent } from 'solid-js'

import { useDimensions } from '~/utils/window'
import { useDimensions, useScreen } from '~/utils/window'

type DrawerProps = {
drawer: JSXElement
Expand All @@ -9,46 +9,70 @@ type DrawerProps = {
onClose?: () => void
}

type OverlayProps = {
open: boolean
onClose?: () => void
}

const PEEK = 56

const Overlay: Component<OverlayProps> = (props) => {
function handleOverlayClick(){
if(props.onClose) props.onClose()
}
return (
<div
class="absolute inset-0 bg-background transition-drawer duration-500"
style={{
'pointer-events': props.open ? undefined : 'none',
opacity: props.open ? 0.5 : 0,
}}
onClick={handleOverlayClick}
/>
)
}


const Drawer: ParentComponent<DrawerProps> = (props) => {
const screen = useScreen()
const dimensions = useDimensions()
const isMobile = () => screen().mobile()
const drawerWidth = isMobile() ? dimensions().width - PEEK : 350
const isOpen = () => !isMobile() || (isMobile() && props.open)

const isMobile = dimensions().width < 500
const drawerWidth = isMobile ? dimensions().width - PEEK : 350
function getContainerStyles (): JSX.CSSProperties {
return {
left: isOpen() ? `${drawerWidth}px` : 0,
width: isMobile() ? '100%' : `${dimensions().width - drawerWidth}px`,
}
}

const onClose = () => props.onClose?.()
function getNavbarStyles (): JSX.CSSProperties {
const opened = isOpen()
return {
left: opened ? 0 : `${-PEEK}px`,
opacity: opened ? 1 : 0.5,
width: `${drawerWidth}px`,
}
}

return (
<>
<div class="relative flex size-full flex-row overflow-hidden">
<nav
class="hide-scrollbar fixed inset-y-0 left-0 h-full w-screen touch-pan-y overflow-y-auto overscroll-y-contain transition-drawer duration-500"
style={{
left: props.open ? 0 : `${-PEEK}px`,
opacity: props.open ? 1 : 0.5,
width: `${drawerWidth}px`,
}}
>
style={getNavbarStyles()}
class="hide-scrollbar fixed inset-y-0 left-0 h-full touch-pan-y overflow-y-auto overscroll-y-contain transition-drawer duration-500">
<div class="flex size-full flex-col rounded-r-lg bg-surface-container-low text-on-surface-variant sm:rounded-r-none">
{props.drawer}
</div>
</nav>

<main
class="absolute inset-y-0 w-screen overflow-y-auto bg-background transition-drawer duration-500"
style={{ left: props.open ? `${drawerWidth}px` : 0 }}
>
style={getContainerStyles()}
class="absolute inset-y-0 flex w-screen flex-1 flex-col overflow-hidden bg-background transition-drawer duration-500">
{props.children}
<div
class="absolute inset-0 bg-background transition-drawer duration-500"
style={{
'pointer-events': props.open ? undefined : 'none',
opacity: props.open ? 0.5 : 0,
}}
onClick={onClose}
/>
{isMobile() && isOpen() && (<Overlay open={isOpen()} onClose={props.onClose}/>)}
</main>
</>
</div>
)
}

Expand Down
5 changes: 5 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
@tailwind components;
@tailwind utilities;

#root{
width: 100vw;
height: 100vh;
}

@layer base {
/* https://m3.material.io/styles/color/roles */
:root {
Expand Down
4 changes: 3 additions & 1 deletion src/pages/dashboard/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import DeviceActivity from './activities/DeviceActivity'
import RouteActivity from './activities/RouteActivity'
import SettingsActivity from './activities/SettingsActivity'
import storage from '~/utils/storage'
import { useScreen } from '~/utils/window'

const PairActivity = lazy(() => import('./activities/PairActivity'))

Expand All @@ -42,11 +43,12 @@ const DashboardDrawer = (props: {
onClose: () => void
devices: Device[] | undefined
}) => {
const screen = useScreen()
return (
<>
<TopAppBar
component="h1"
leading={<IconButton onClick={props.onClose}>arrow_back</IconButton>}
leading={(screen().mobile() && <IconButton onClick={props.onClose}>arrow_back</IconButton>)}
>
comma connect
</TopAppBar>
Expand Down
88 changes: 46 additions & 42 deletions src/pages/dashboard/activities/DeviceActivity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { getDeviceName } from '~/utils/device'

import RouteList from '../components/RouteList'
import { DashboardContext } from '../Dashboard'
import { useScreen } from '~/utils/window'

type DeviceActivityProps = {
dongleId: string
Expand All @@ -25,6 +26,7 @@ interface SnapshotResponse {
}

const DeviceActivity: VoidComponent<DeviceActivityProps> = (props) => {
const screen = useScreen()
const { toggleDrawer } = useContext(DashboardContext)!

const [device] = createResource(() => props.dongleId, getDevice)
Expand Down Expand Up @@ -104,59 +106,61 @@ const DeviceActivity: VoidComponent<DeviceActivityProps> = (props) => {
return (
<>
<TopAppBar
leading={<IconButton onClick={toggleDrawer}>menu</IconButton>}
leading={(screen().mobile() && <IconButton onClick={toggleDrawer}>menu</IconButton>)}
trailing={<IconButton href={`/${props.dongleId}/settings`}>settings</IconButton>}
>
{deviceName()}
</TopAppBar>
<div class="flex flex-col gap-4 px-4 pb-4">
<div class="h-min overflow-hidden rounded-lg bg-surface-container-low">
<div class="flex">
<div class="flex-auto">
<Suspense fallback={<div class="skeleton-loader size-full" />}>
<div class="p-4">
<DeviceStatistics dongleId={props.dongleId} />
</div>
</Suspense>
</div>
<div class="flex p-4">
<IconButton onClick={() => void takeSnapshot()}>camera</IconButton>
<div class="flex-1 overflow-y-auto">
<div class="flex flex-col gap-4 px-4 pb-4">
<div class="h-min overflow-hidden rounded-lg bg-surface-container-low">
<div class="flex">
<div class="flex-auto">
<Suspense fallback={<div class="skeleton-loader size-full" />}>
<div class="p-4">
<DeviceStatistics dongleId={props.dongleId} />
</div>
</Suspense>
</div>
<div class="flex p-4">
<IconButton onClick={() => void takeSnapshot()}>camera</IconButton>
</div>
</div>
</div>
</div>
<div class="flex flex-col gap-2">
<For each={snapshot().images}>
{(image, index) => (
<div class="flex-1 overflow-hidden rounded-lg bg-surface-container-low">
<div class="relative p-4">
<img src={`data:image/jpeg;base64,${image}`} alt={`Device Snapshot ${index() + 1}`} />
<div class="absolute right-4 top-4 p-4">
<IconButton onClick={() => downloadSnapshot(image, index())} class="text-white">download</IconButton>
<IconButton onClick={() => clearImage(index())} class="text-white">clear</IconButton>
<div class="flex flex-col gap-2">
<For each={snapshot().images}>
{(image, index) => (
<div class="flex-1 overflow-hidden rounded-lg bg-surface-container-low">
<div class="relative p-4">
<img src={`data:image/jpeg;base64,${image}`} alt={`Device Snapshot ${index() + 1}`} />
<div class="absolute right-4 top-4 p-4">
<IconButton onClick={() => downloadSnapshot(image, index())} class="text-white">download</IconButton>
<IconButton onClick={() => clearImage(index())} class="text-white">clear</IconButton>
</div>
</div>
</div>
)}
</For>
{snapshot().fetching && (
<div class="flex-1 overflow-hidden rounded-lg bg-surface-container-low">
<div class="p-4">
<div>Loading snapshots...</div>
</div>
</div>
)}
</For>
{snapshot().fetching && (
<div class="flex-1 overflow-hidden rounded-lg bg-surface-container-low">
<div class="p-4">
<div>Loading snapshots...</div>
</div>
</div>
)}
{snapshot().error && (
<div class="flex-1 overflow-hidden rounded-lg bg-surface-container-low">
<div class="flex items-center p-4">
<IconButton onClick={clearError} class="text-white">Clear</IconButton>
<span>Error: {snapshot().error}</span>
{snapshot().error && (
<div class="flex-1 overflow-hidden rounded-lg bg-surface-container-low">
<div class="flex items-center p-4">
<IconButton onClick={clearError} class="text-white">Clear</IconButton>
<span>Error: {snapshot().error}</span>
</div>
</div>
</div>
)}
</div>
<div class="flex flex-col gap-2">
<span class="text-label-sm">Routes</span>
<RouteList dongleId={props.dongleId} />
)}
</div>
<div class="flex flex-col gap-2">
<span class="text-label-sm">Routes</span>
<RouteList dongleId={props.dongleId} />
</div>
</div>
</div>
</>
Expand Down
32 changes: 32 additions & 0 deletions src/utils/breakpoints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const MIN_SIZE = {
xs: 0, // phone
sm: 500, // tablet
md: 900, // small laptop
lg: 1200, // desktop
xl: 1536, // large screen
}

export type Size = keyof typeof MIN_SIZE

function up(size: Size) {
return `@media (min-width:${MIN_SIZE[size]}px)`
}

function down(size: Size) {
const width = MIN_SIZE[size]
const ubound = Object.values(MIN_SIZE).find((val) => val > width)
if (ubound && ubound > 0) {
return `@media (max-width:${ubound}px)`
} else {
return `@media (max-width:${MIN_SIZE.xl * 1.2}px)`
}
}

function only(size: Size) {
const excludeup = up(size).replace(/^@media( ?)/m, '')
const excludedown = down(size).replace(/^@media( ?)/m, '')
return `@media ${excludeup} and ${excludedown}`
}

export default { values: MIN_SIZE, up, down, only }

36 changes: 35 additions & 1 deletion src/utils/window.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import { createSignal, onCleanup, onMount } from 'solid-js'
import { createSignal, createMemo, createEffect, onCleanup, onMount } from 'solid-js'
import type { Accessor } from 'solid-js'
import breakpoints from './breakpoints'

type Dimensions = { width: number; height: number }

const match = (query: string): boolean => {
if (typeof window === 'undefined') return true
if (typeof window.matchMedia !== 'function') return true
const media = window.matchMedia(query.replace(/^@media( ?)/m, ''))
return media.matches
}

export const getDimensions = (): Dimensions => {
if (typeof window === 'undefined') return { width: 0, height: 0 }
const { innerWidth: width, innerHeight: height } = window
Expand All @@ -20,3 +28,29 @@ export const useDimensions = (): Accessor<Dimensions> => {

return dimensions
}

export function useMediaQuery(query: string) {
const [matches, setMatches] = createSignal(false)

const listener = () => {
setMatches(match(query))
}

createEffect(() => {
listener()
window.addEventListener('resize', listener)
return () => window.removeEventListener('resize', listener)
})

return matches
}

export function useScreen() {
const desktop = useMediaQuery(breakpoints.up('lg'))
const tablet = useMediaQuery(breakpoints.only('md'))
const mobile = useMediaQuery(breakpoints.down('sm'))
return createMemo(() => {
return {mobile: mobile, tablet: tablet, desktop: desktop}
})
}