Skip to content

Commit

Permalink
Terminav 2.0 (#124)
Browse files Browse the repository at this point in the history
* almost there, just gotta finalize touch event triggers

* terminav triggers more-or-less finalized

* disable Terminav interactivity on touch devices

* disable scrolling on body when Terminav is visible

* fix NPE in server-side rendering
  • Loading branch information
dabrady committed Sep 23, 2023
1 parent 0c4e9e6 commit b4a1d7f
Show file tree
Hide file tree
Showing 8 changed files with 281 additions and 117 deletions.
56 changes: 29 additions & 27 deletions src/components/BaseContentLayout.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,35 +9,37 @@ import Terminav from "@components/Terminav";
export default function BaseContentLayout({ children, className }) {
return (
<CoreLayout>
<Container
className={className}
sx={{
maxHeight: 'none',
width: 'inherit',
maxWidth: ['100vw', '85vw', '60vw'],
margin: ['1.5rem', '0 2.5rem 2.5rem 12rem'],
paddingTop: [0, '2rem'],
}}
>
{/* Main Content */}
<Box sx={{
borderTop: '1px solid',
paddingTop: [0, '0.8rem'],
<Box sx={{ position: 'relative' }}>
<Container
className={className}
sx={{
maxHeight: 'none',
width: 'inherit',
maxWidth: ['100vw', '85vw', '60vw'],
margin: ['1.5rem', '0 2.5rem 2.5rem 12rem'],
paddingTop: [0, '2rem'],
}}
>
{/* Main Content */}
<Box sx={{
borderTop: '1px solid',
paddingTop: [0, '0.8rem'],

'& > h1:nth-of-type(1)': {
color: 'accent',
paddingTop: '0.4rem',
marginBottom: '2rem',
},
}}>
{children}
</Box>
'& > h1:nth-of-type(1)': {
color: 'accent',
paddingTop: '0.4rem',
marginBottom: '2rem',
},
}}>
{children}
</Box>

{/* Footer */}
<Box sx={{ paddingTop: '2rem' }}>
<Terminav />
</Box>
</Container>
{/* Footer */}
{/* <Box sx={{ paddingTop: '2rem' }}> */}
{/* <Terminav /> */}
{/* </Box> */}
</Container>
</Box>
</CoreLayout>
);
}
4 changes: 4 additions & 0 deletions src/components/CoreLayout.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
/** @jsxImportSource theme-ui */

import SiteHelmet from '@components/SiteHelmet';
import Terminav from "@components/Terminav";

export default function CoreLayout({ children }) {
return (
<>
<SiteHelmet/>
{/* <SEO title="Home" keywords={[`gatsby`, `application`, `react`]} /> */}
{/* Show the Terminav when a user _tries_ to scroll */}
<Terminav scrollVisibilityThreshold={0}/>
{children}
</>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/FullscreenNoScrollLayout.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import { Global, Container } from "theme-ui";
import { useState } from 'react';

import CoreLayout from '@components/CoreLayout';
import Terminav from "@components/Terminav";

export default function FullscreenNoScrollLayout({ children, className }) {
return (
<CoreLayout>
<Global styles={{
// Reset some 'body' global overrides
// TODO(dabrady) Don't do this, just redesign the home page layout.
body: {
marginLeft: 'inherit',
marginRight: 'inherit',
Expand Down
214 changes: 128 additions & 86 deletions src/components/Terminav.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@
import { navigate } from 'gatsby';
import _ from 'lodash';
import { useEffect, useRef, useState } from 'react';
import { Box, Flex, Input, Label, NavLink, Paragraph } from 'theme-ui';
import { Box, Container, Flex, Input, Label, NavLink, Paragraph } from 'theme-ui';

import NavLinks from '@content/navlinks.yaml';

import theme from '@styles/theme';

import isTouchDevice from '@utils/isTouchDevice';
import useEdgeScrollListener from '@utils/hooks/useEdgeScrollListener.hook';

const NAV_LINKS = _.transform(
NavLinks,
function makeNavLink(acc, { label, path }) {
Expand All @@ -31,17 +34,25 @@ const COMMANDS = {
['help']: function listAvailableCommands() {
return <MonoList items={_.without(_.keys(COMMANDS), UNKNOWN_COMMAND)}/>;
},
['cd']: function navigateTo(link) {
['cd']: function navigateTo(link, close) {
// Just reset if they try to navigate to the current page.
if (link == currentPageLink()) {
// Close the terminav.
close();
// Prep the display text for the next time they open it.
return COMMANDS['ls']();
}

// Navigate to the link destination.
if (link in NAV_LINKS) {
navigate(NAV_LINKS[link].props.href);
return null;
return close();
}

// Bonus feature: `cd -` acts as a back-button.
if (link == '-') {
navigate(-1);
return null;
return close();
}

// If you don't provide a known link, we show you your options.
Expand All @@ -64,94 +75,121 @@ const COMMANDS = {

/** A navigation component with the look and feel of a terminal. */
export default function Terminav({ scrollVisibilityThreshold = 85 }) {
// NOTE(dabrady) Certain interactivity is not suited to touch interfaces.
var interactive = !isTouchDevice();

var inputRef = useRef();
var [output, setOutput] = useState(COMMANDS.ls());
var [opacity, setOpacity] = useState(0);

var [opacity, _setOpacity] = useState(0);
function show() {
_setOpacity(1);
// Prevent body from scrolling underneath.
document.body.setAttribute('style', 'overflow: hidden');
}
function hide() {
_setOpacity(0);
// Reactivate scrolling on body.
document.body.removeAttribute('style');
}

/**
* This effect makes the Terminav fade in based on user's scroll position.
* This effect makes the Terminav fade in when you scroll past the edge of the screen.
*/
useEffect(function() {
function adjustOpacity(ev) {
var focused = inputRef?.current && document.activeElement == inputRef.current;
if (focused) return;

var root = document.documentElement;
var navPosition = inputRef.current.getBoundingClientRect();
var navInView = navPosition.bottom <= root.clientHeight;

var maxScrollPosition = root.scrollHeight - root.clientHeight;
var currentScrollPosition = root.scrollTop;
var scrollProgress = (currentScrollPosition / maxScrollPosition) * 100;

if (
maxScrollPosition == 0
|| isNaN(scrollProgress)
|| (navInView && scrollProgress >= scrollVisibilityThreshold)
) {
setOpacity(1);
// Focus on visible when not on touch devices.
// Input focus on touch devices tends to automatically open a keyboard,
// and that's annoying.
if (ev.type == 'wheel' && navInView) {
inputRef.current.focus();
}
} else {
setOpacity(0);
}
}

// NOTE(dabrady) Using 'wheel' event instead of 'scroll' here because it
// fires when a user _attempts_ to scroll, even if the page is not scrollable.
// The 'scroll' event only fires when the page actually scrolls.
window.addEventListener('wheel', adjustOpacity);
window.addEventListener('touchmove', adjustOpacity);
return function stopListening() {
window.removeEventListener('wheel', adjustOpacity);
window.removeEventListener('touchmove', adjustOpacity);
};
}, []);
useEdgeScrollListener({
handler: function showTerminav() {
show();
// Focus once visible.
setTimeout(function focusTerminav() {
inputRef.current?.focus();
}, 250);
},
pauseWhen: function terminavIsVisible() {
return opacity == 1;
},
deps: [opacity],
});

return (
<Box sx={{
// NOTE(dabrady) Currently, the content of this navbar will be at most 3
// lines of body text, so using that plus a bit extra to give breathing room.
height: ({ lineHeights }) => `calc(${lineHeights.body} * 4rem)`,
marginBottom: '0.6rem',
visibility: opacity ? 'visible' : 'hidden',
opacity,
transition: 'visibility 0.3s linear, opacity 0.3s linear',
}}>
<Flex
as='form'
spellCheck={false}
autoComplete='off'
onSubmit={function processInput(e) {
e.preventDefault();

var input = inputRef.current.value;
var [commandName, ...args] = input.split(' ');
var command = _.get(COMMANDS, commandName, COMMANDS['_UNKNOWN_']);
setOutput(() => command(...args));
inputRef.current.value = '';
/* Fullscreen overlay */
<Box
sx={{
position: 'fixed',
inset: 0,
backdropFilter: 'blur(20px)',
zIndex: 9001,
visibility: opacity ? 'visible' : 'hidden',
opacity,
transition: 'visibility 0.3s linear, opacity 0.3s linear',
}}
onClick={hide}
>
{/* Overlay body (for relative positioning of contents) */}
<Container
sx={{
maxHeight: 'none',
width: 'inherit',
maxWidth: '1000px',
}}
>
<Label
htmlFor='terminav-input'
sx={{
flex: 1,
fontFamily: 'monospace'
}}
></Label>
<Input
id='terminav-input'
name='terminav-input'
ref={inputRef}
placeholder='explore...'
sx={{ fontFamily: 'monospace' }}
/>
</Flex>
<Box>{output}</Box>
{/* Overlay contents */}
<Box sx={{
margin: ['5.5rem 3.5rem', '4.8rem 2.5rem 2.5rem 12rem'],
}}>
<Box sx={{
// NOTE(dabrady) Currently, the content of this navbar will be at most 3
// lines of body text, so using that plus a bit extra to give breathing room.
height: ({ lineHeights }) => `calc(${lineHeights.body} * 4rem)`,
marginBottom: '0.6rem',
}}>
<Flex
as='form'
spellCheck={false}
autoComplete='off'
onSubmit={function processInput(e) {
e.preventDefault();

var input = inputRef.current.value;
var [commandName, ...args] = input.split(' ');
var command = _.get(COMMANDS, commandName, COMMANDS['_UNKNOWN_']);
setOutput(() => command(...args, hide));
inputRef.current.value = '';
}}
>
<Label
htmlFor='terminav-input'
sx={{
flex: 1,
fontFamily: 'monospace',
margin: 0,
padding: 0,
display: 'inline',
width: 'fit-content'
}}
>{interactive
? '➜'
: <span
sx={{
color: 'accent',
fontFamily: 'monospace',
}}
>
./
</span>
}</Label>
{interactive &&
<Input
id='terminav-input'
name='terminav-input'
ref={inputRef}
placeholder='explore...'
sx={{ fontFamily: 'monospace', color: 'accent' }}
/>}
</Flex>
<Box>{output}</Box>
</Box>
</Box>
</Container>
</Box>
);
}
Expand Down Expand Up @@ -216,9 +254,13 @@ function MonoList({ heading, items }) {
);
}

/** Finds the nav link to the current page. */
function currentPageLink() {
var currentPage = typeof window != 'undefined' ? window.location.pathname : '';
return _.findKey(NAV_LINKS, ['props.href', currentPage]);
}

/** Filters out the current page from the set of possible nav links. */
function availableNavLinks() {
var currentPage = typeof window != 'undefined' ? window.location.pathname : '';
var currentPageLink = _.findKey(NAV_LINKS, ['props.href', currentPage]);
return _.omit(NAV_LINKS, currentPageLink);
return _.omit(NAV_LINKS, currentPageLink());
}
2 changes: 1 addition & 1 deletion src/pages/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export default function Home() {
<FullscreenNoScrollLayout>
{/* <SEO title="Home" keywords={[`gatsby`, `application`, `react`]} /> */}
{/* Show the Terminav when a user _tries_ to scroll */}
<Terminav scrollVisibilityThreshold={0}/>
{/* <Terminav scrollVisibilityThreshold={0}/> */}
<Box
sx={{
position: "absolute",
Expand Down
6 changes: 4 additions & 2 deletions src/styles/theme.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,6 @@ const theme = {
width: '100%',
maxWidth: '1000px',
minHeight: '100vh; min-height: -webkit-fill-available',
position: 'relative',
},

li: {
Expand Down Expand Up @@ -319,7 +318,7 @@ const theme = {
backgroundColor: "transparent",
width: "100%",
padding: '0',
marginLeft: '1rem',
marginLeft: '1.2rem',

animation: "1ms void-animation-out",
appearance: "none",
Expand Down Expand Up @@ -378,6 +377,9 @@ const theme = {
paddingLeft: '2.5rem',
paddingBottom: ['0.6rem', '0.4rem'],
borderLeft: '1px solid',
"&:first-child": {
paddingTop: '0.4rem',
},
"&:nth-last-of-type(2)": {
paddingBottom: ['1.45rem', '1.25rem'],
},
Expand Down
Loading

0 comments on commit b4a1d7f

Please sign in to comment.