Fixed some layout issues

This commit is contained in:
Michael Jackson 2020-06-03 11:45:16 -07:00
parent 7215072c72
commit 33edf5beec
7 changed files with 365 additions and 280 deletions

View File

@ -5,8 +5,7 @@ import PropTypes from 'prop-types';
import { fontSans, fontMono } from '../utils/style.js'; import { fontSans, fontMono } from '../utils/style.js';
import { PackageInfoProvider } from './PackageInfo.js'; import FolderViewer from './FolderViewer.js';
import DirectoryViewer from './DirectoryViewer.js';
import FileViewer from './FileViewer.js'; import FileViewer from './FileViewer.js';
import { TwitterIcon, GitHubIcon } from './Icons.js'; import { TwitterIcon, GitHubIcon } from './Icons.js';
@ -128,30 +127,52 @@ function Link({ css, ...rest }) {
css={{ css={{
color: '#0076ff', color: '#0076ff',
textDecoration: 'none', textDecoration: 'none',
':hover': { ':hover': { textDecoration: 'underline' },
textDecoration: 'underline'
},
...css ...css
}} }}
/> />
); );
} }
export default function App({ function AppHeader() {
return (
<header css={{ marginTop: '2rem' }}>
<h1
css={{
textAlign: 'center',
fontSize: '3rem',
letterSpacing: '0.05em'
}}
>
<a href="/" css={{ color: '#000', textDecoration: 'none' }}>
UNPKG
</a>
</h1>
{/*
<nav>
<Link href="#" css={{ color: '#c400ff' }}>
Become a Sponsor
</Link>
</nav>
*/}
</header>
);
}
function AppNavigation({
packageName, packageName,
packageVersion, packageVersion,
availableVersions = [], availableVersions,
filename, filename
target
}) { }) {
function handleChange(event) { function handleVersionChange(nextVersion) {
window.location.href = window.location.href.replace( window.location.href = window.location.href.replace(
'@' + packageVersion, '@' + packageVersion,
'@' + event.target.value '@' + nextVersion
); );
} }
const breadcrumbs = []; let breadcrumbs = [];
if (filename === '/') { if (filename === '/') {
breadcrumbs.push(packageName); breadcrumbs.push(packageName);
@ -160,12 +181,11 @@ export default function App({
breadcrumbs.push(<Link href={`${url}/`}>{packageName}</Link>); breadcrumbs.push(<Link href={`${url}/`}>{packageName}</Link>);
const segments = filename let segments = filename
.replace(/^\/+/, '') .replace(/^\/+/, '')
.replace(/\/+$/, '') .replace(/\/+$/, '')
.split('/'); .split('/');
let lastSegment = segments.pop();
const lastSegment = segments.pop();
segments.forEach(segment => { segments.forEach(segment => {
url += `/${segment}`; url += `/${segment}`;
@ -175,8 +195,126 @@ export default function App({
breadcrumbs.push(lastSegment); breadcrumbs.push(lastSegment);
} }
// TODO: Provide a user pref to go full width? return (
const maxContentWidth = 940; <header
css={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
'@media (max-width: 700px)': {
flexDirection: 'column-reverse',
alignItems: 'flex-start'
}
}}
>
<h1
css={{
fontSize: '1.5rem',
fontWeight: 'normal',
flex: 1,
wordBreak: 'break-all'
}}
>
<nav>
{breadcrumbs.map((item, index, array) => (
<Fragment key={index}>
{index !== 0 && (
<span css={{ paddingLeft: 5, paddingRight: 5 }}>/</span>
)}
{index === array.length - 1 ? <strong>{item}</strong> : item}
</Fragment>
))}
</nav>
</h1>
<PackageVersionPicker
packageVersion={packageVersion}
availableVersions={availableVersions}
onChange={handleVersionChange}
/>
</header>
);
}
function PackageVersionPicker({ packageVersion, availableVersions, onChange }) {
function handleChange(event) {
if (onChange) onChange(event.target.value);
}
return (
<p
css={{
marginLeft: 20,
'@media (max-width: 700px)': {
marginLeft: 0,
marginBottom: 0
}
}}
>
<label>
Version:{' '}
<select
name="version"
defaultValue={packageVersion}
onChange={handleChange}
css={{
appearance: 'none',
cursor: 'pointer',
padding: '4px 24px 4px 8px',
fontWeight: 600,
fontSize: '0.9em',
color: '#24292e',
border: '1px solid rgba(27,31,35,.2)',
borderRadius: 3,
backgroundColor: '#eff3f6',
backgroundImage: `url(${SelectDownArrow})`,
backgroundPosition: 'right 8px center',
backgroundRepeat: 'no-repeat',
backgroundSize: 'auto 25%',
':hover': {
backgroundColor: '#e6ebf1',
borderColor: 'rgba(27,31,35,.35)'
},
':active': {
backgroundColor: '#e9ecef',
borderColor: 'rgba(27,31,35,.35)',
boxShadow: 'inset 0 0.15em 0.3em rgba(27,31,35,.15)'
}
}}
>
{availableVersions.map(v => (
<option key={v} value={v}>
{v}
</option>
))}
</select>
</label>
</p>
);
}
function AppContent({ packageName, packageVersion, target }) {
return target.type === 'directory' ? (
<FolderViewer path={target.path} details={target.details} />
) : target.type === 'file' ? (
<FileViewer
packageName={packageName}
packageVersion={packageVersion}
path={target.path}
details={target.details}
/>
) : null;
}
export default function App({
packageName,
packageVersion,
availableVersions = [],
filename,
target
}) {
let maxContentWidth = 940;
// TODO: Make this changeable
let isFullWidth = false;
return ( return (
<Fragment> <Fragment>
@ -191,115 +329,25 @@ export default function App({
margin: '0 auto' margin: '0 auto'
}} }}
> >
<header css={{ marginTop: '2rem' }}> <AppHeader />
<h1
css={{
textAlign: 'center',
fontSize: '3rem',
letterSpacing: '0.05em'
}}
>
<a href="/" css={{ color: '#000', textDecoration: 'none' }}>
UNPKG
</a>
</h1>
{/*
<nav>
<Link href="#" css={{ color: '#c400ff' }}>
Become a Sponsor
</Link>
</nav>
*/}
</header>
<header
css={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
'@media (max-width: 700px)': {
flexDirection: 'column-reverse',
alignItems: 'flex-start'
}
}}
>
<h1
css={{
fontSize: '1.5rem',
fontWeight: 'normal',
flex: 1,
wordBreak: 'break-all'
}}
>
<nav>
{breadcrumbs.map((item, index, array) => (
<Fragment key={index}>
{index !== 0 && (
<span css={{ paddingLeft: 5, paddingRight: 5 }}>/</span>
)}
{index === array.length - 1 ? (
<strong>{item}</strong>
) : (
item
)}
</Fragment>
))}
</nav>
</h1>
<p
css={{
marginLeft: 20,
'@media (max-width: 700px)': {
marginLeft: 0,
marginBottom: 0
}
}}
>
<label>
Version:{' '}
<select
name="version"
defaultValue={packageVersion}
onChange={handleChange}
css={{
appearance: 'none',
cursor: 'pointer',
padding: '4px 24px 4px 8px',
fontWeight: 600,
fontSize: '0.9em',
color: '#24292e',
border: '1px solid rgba(27,31,35,.2)',
borderRadius: 3,
backgroundColor: '#eff3f6',
backgroundImage: `url(${SelectDownArrow})`,
backgroundPosition: 'right 8px center',
backgroundRepeat: 'no-repeat',
backgroundSize: 'auto 25%',
':hover': {
backgroundColor: '#e6ebf1',
borderColor: 'rgba(27,31,35,.35)'
},
':active': {
backgroundColor: '#e9ecef',
borderColor: 'rgba(27,31,35,.35)',
boxShadow: 'inset 0 0.15em 0.3em rgba(27,31,35,.15)'
}
}}
>
{availableVersions.map(v => (
<option key={v} value={v}>
{v}
</option>
))}
</select>
</label>
</p>
</header>
</div> </div>
<div <div
css={{ css={{
maxWidth: maxContentWidth, maxWidth: isFullWidth ? undefined : maxContentWidth,
padding: '0 20px',
margin: '0 auto'
}}
>
<AppNavigation
packageName={packageName}
packageVersion={packageVersion}
availableVersions={availableVersions}
filename={filename}
/>
</div>
<div
css={{
maxWidth: isFullWidth ? undefined : maxContentWidth,
padding: '0 20px', padding: '0 20px',
margin: '0 auto', margin: '0 auto',
'@media (max-width: 700px)': { '@media (max-width: 700px)': {
@ -308,16 +356,11 @@ export default function App({
} }
}} }}
> >
<PackageInfoProvider <AppContent
packageName={packageName} packageName={packageName}
packageVersion={packageVersion} packageVersion={packageVersion}
> target={target}
{target.type === 'directory' ? ( />
<DirectoryViewer path={target.path} details={target.details} />
) : target.type === 'file' ? (
<FileViewer path={target.path} details={target.details} />
) : null}
</PackageInfoProvider>
</div> </div>
</div> </div>
@ -342,7 +385,6 @@ export default function App({
<p>&copy; {new Date().getFullYear()} UNPKG</p> <p>&copy; {new Date().getFullYear()} UNPKG</p>
<p css={{ fontSize: '1.5rem' }}> <p css={{ fontSize: '1.5rem' }}>
<a <a
title="Twitter"
href="https://twitter.com/unpkg" href="https://twitter.com/unpkg"
css={{ css={{
color: '#aaa', color: '#aaa',
@ -353,13 +395,12 @@ export default function App({
<TwitterIcon /> <TwitterIcon />
</a> </a>
<a <a
title="GitHub"
href="https://github.com/mjackson/unpkg" href="https://github.com/mjackson/unpkg"
css={{ css={{
color: '#aaa', color: '#aaa',
display: 'inline-block', display: 'inline-block',
marginLeft: '1rem', ':hover': { color: 'white' },
':hover': { color: 'white' } marginLeft: '1rem'
}} }}
> >
<GitHubIcon /> <GitHubIcon />

View File

@ -0,0 +1,49 @@
/** @jsx jsx */
import { jsx } from '@emotion/core';
const maxWidth = 700;
export function ContentArea({ children, css }) {
return (
<div
css={{
border: '1px solid #dfe2e5',
borderRadius: 3,
[`@media (max-width: ${maxWidth}px)`]: {
borderRightWidth: 0,
borderLeftWidth: 0
},
...css
}}
>
{children}
</div>
);
}
export function ContentAreaHeaderBar({ children, css }) {
return (
<div
css={{
padding: 10,
background: '#f6f8fa',
color: '#424242',
border: '1px solid #d1d5da',
borderTopLeftRadius: 3,
borderTopRightRadius: 3,
margin: '-1px -1px 0',
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
[`@media (max-width: ${maxWidth}px)`]: {
paddingRight: 20,
paddingLeft: 20
},
...css
}}
>
{children}
</div>
);
}

View File

@ -5,10 +5,10 @@ import PropTypes from 'prop-types';
import { formatBytes } from '../utils/format.js'; import { formatBytes } from '../utils/format.js';
import { createHTML } from '../utils/markup.js'; import { createHTML } from '../utils/markup.js';
import { usePackageInfo } from './PackageInfo.js'; import { ContentArea, ContentAreaHeaderBar } from './ContentArea.js';
function getBasename(path) { function getBasename(path) {
const segments = path.split('/'); let segments = path.split('/');
return segments[segments.length - 1]; return segments[segments.length - 1];
} }
@ -21,8 +21,8 @@ function ImageViewer({ path, uri }) {
} }
function CodeListing({ highlights }) { function CodeListing({ highlights }) {
const lines = highlights.slice(0); let lines = highlights.slice(0);
const hasTrailingNewline = lines.length && lines[lines.length - 1] === ''; let hasTrailingNewline = lines.length && lines[lines.length - 1] === '';
if (hasTrailingNewline) { if (hasTrailingNewline) {
lines.pop(); lines.pop();
} }
@ -46,7 +46,7 @@ function CodeListing({ highlights }) {
> >
<tbody> <tbody>
{lines.map((line, index) => { {lines.map((line, index) => {
const lineNumber = index + 1; let lineNumber = index + 1;
return ( return (
<tr key={index}> <tr key={index}>
@ -120,71 +120,48 @@ function BinaryViewer() {
); );
} }
export default function FileViewer({ path, details }) { export default function FileViewer({
const { packageName, packageVersion } = usePackageInfo(); packageName,
const { highlights, uri, language, size } = details; packageVersion,
path,
const segments = path.split('/'); details
const filename = segments[segments.length - 1]; }) {
let { highlights, uri, language, size } = details;
return ( return (
<div <ContentArea>
css={{ <ContentAreaHeaderBar>
border: '1px solid #dfe2e5', <span>{formatBytes(size)}</span>
borderRadius: 3, <span>{language}</span>
'@media (max-width: 700px)': { <span>
borderRightWidth: 0, <a
borderLeftWidth: 0 href={`/${packageName}@${packageVersion}${path}`}
} css={{
}} display: 'inline-block',
> marginLeft: 8,
<div padding: '2px 8px',
css={{ textDecoration: 'none',
padding: 10, fontWeight: 600,
background: '#f6f8fa', fontSize: '0.9rem',
color: '#424242', color: '#24292e',
border: '1px solid #d1d5da', backgroundColor: '#eff3f6',
borderTopLeftRadius: 3, border: '1px solid rgba(27,31,35,.2)',
borderTopRightRadius: 3, borderRadius: 3,
margin: '-1px -1px 0', ':hover': {
display: 'flex', backgroundColor: '#e6ebf1',
flexDirection: 'row', borderColor: 'rgba(27,31,35,.35)'
alignItems: 'center', },
justifyContent: 'space-between', ':active': {
'@media (max-width: 700px)': { backgroundColor: '#e9ecef',
paddingRight: 20, borderColor: 'rgba(27,31,35,.35)',
paddingLeft: 20 boxShadow: 'inset 0 0.15em 0.3em rgba(27,31,35,.15)'
} }
}} }}
> >
<span>{formatBytes(size)}</span> <span>{language}</span>{' '} View Raw
<a </a>
title={filename} </span>
href={`/${packageName}@${packageVersion}${path}`} </ContentAreaHeaderBar>
css={{
display: 'inline-block',
textDecoration: 'none',
padding: '2px 8px',
fontWeight: 600,
fontSize: '0.9rem',
color: '#24292e',
backgroundColor: '#eff3f6',
border: '1px solid rgba(27,31,35,.2)',
borderRadius: 3,
':hover': {
backgroundColor: '#e6ebf1',
borderColor: 'rgba(27,31,35,.35)'
},
':active': {
backgroundColor: '#e9ecef',
borderColor: 'rgba(27,31,35,.35)',
boxShadow: 'inset 0 0.15em 0.3em rgba(27,31,35,.15)'
}
}}
>
View Raw
</a>
</div>
{highlights ? ( {highlights ? (
<CodeListing highlights={highlights} /> <CodeListing highlights={highlights} />
@ -193,7 +170,7 @@ export default function FileViewer({ path, details }) {
) : ( ) : (
<BinaryViewer /> <BinaryViewer />
)} )}
</div> </ContentArea>
); );
} }

View File

@ -6,7 +6,8 @@ import sortBy from 'sort-by';
import { formatBytes } from '../utils/format.js'; import { formatBytes } from '../utils/format.js';
import { DirectoryIcon, CodeFileIcon } from './Icons.js'; import { ContentArea, ContentAreaHeaderBar } from './ContentArea.js';
import { FolderIcon, FileIcon, FileCodeIcon } from './Icons.js';
const linkStyle = { const linkStyle = {
color: '#0076ff', color: '#0076ff',
@ -48,7 +49,26 @@ function getRelName(path, base) {
return path.substr(base.length > 1 ? base.length + 1 : 1); return path.substr(base.length > 1 ? base.length + 1 : 1);
} }
export default function DirectoryViewer({ path, details: entries }) { export default function FolderViewer({ path, details: entries }) {
const { subdirs, files } = Object.keys(entries).reduce(
(memo, key) => {
const { subdirs, files } = memo;
const entry = entries[key];
if (entry.type === 'directory') {
subdirs.push(entry);
} else if (entry.type === 'file') {
files.push(entry);
}
return memo;
},
{ subdirs: [], files: [] }
);
subdirs.sort(sortBy('path'));
files.sort(sortBy('path'));
const rows = []; const rows = [];
if (path !== '/') { if (path !== '/') {
@ -66,30 +86,14 @@ export default function DirectoryViewer({ path, details: entries }) {
); );
} }
const { subdirs, files } = Object.keys(entries).reduce( subdirs.forEach(({ path: dirname }) => {
(memo, key) => {
const { subdirs, files } = memo;
const entry = entries[key];
if (entry.type === 'directory') {
subdirs.push(entry);
} else if (entry.type === 'file') {
files.push(entry);
}
return memo;
},
{ subdirs: [], files: [] }
);
subdirs.sort(sortBy('path')).forEach(({ path: dirname }) => {
const relName = getRelName(dirname, path); const relName = getRelName(dirname, path);
const href = relName + '/'; const href = relName + '/';
rows.push( rows.push(
<tr key={relName}> <tr key={relName}>
<td css={iconCellStyle}> <td css={iconCellStyle}>
<DirectoryIcon /> <FolderIcon />
</td> </td>
<td css={tableCellStyle}> <td css={tableCellStyle}>
<a title={relName} href={href} css={linkStyle}> <a title={relName} href={href} css={linkStyle}>
@ -102,40 +106,44 @@ export default function DirectoryViewer({ path, details: entries }) {
); );
}); });
files files.forEach(({ path: filename, size, contentType }) => {
.sort(sortBy('path')) const relName = getRelName(filename, path);
.forEach(({ path: filename, size, contentType }) => { const href = relName;
const relName = getRelName(filename, path);
const href = relName;
rows.push( rows.push(
<tr key={relName}> <tr key={relName}>
<td css={iconCellStyle}> <td css={iconCellStyle}>
<CodeFileIcon /> {contentType === 'text/plain' || contentType === 'text/markdown' ? (
</td> <FileIcon />
<td css={tableCellStyle}> ) : (
<a title={relName} href={href} css={linkStyle}> <FileCodeIcon />
{relName} )}
</a> </td>
</td> <td css={tableCellStyle}>
<td css={tableCellStyle}>{formatBytes(size)}</td> <a title={relName} href={href} css={linkStyle}>
<td css={typeCellStyle}>{contentType}</td> {relName}
</tr> </a>
); </td>
}); <td css={tableCellStyle}>{formatBytes(size)}</td>
<td css={typeCellStyle}>{contentType}</td>
</tr>
);
});
let counts = [];
if (files.length > 0) {
counts.push(`${files.length} file${files.length === 1 ? '' : 's'}`);
}
if (subdirs.length > 0) {
counts.push(`${subdirs.length} folder${subdirs.length === 1 ? '' : 's'}`);
}
return ( return (
<div <ContentArea>
css={{ <ContentAreaHeaderBar>
border: '1px solid #dfe2e5', <span>{counts.join(', ')}</span>
borderRadius: 3, </ContentAreaHeaderBar>
borderTopWidth: 0,
'@media (max-width: 700px)': {
borderRightWidth: 0,
borderLeftWidth: 0
}
}}
>
<table <table
css={{ css={{
width: '100%', width: '100%',
@ -146,6 +154,9 @@ export default function DirectoryViewer({ path, details: entries }) {
'& th + th + th + th, & td + td + td + td': { '& th + th + th + th, & td + td + td + td': {
display: 'none' display: 'none'
} }
},
'& tr:first-child td': {
borderTop: 0
} }
}} }}
> >
@ -167,12 +178,12 @@ export default function DirectoryViewer({ path, details: entries }) {
</thead> </thead>
<tbody>{rows}</tbody> <tbody>{rows}</tbody>
</table> </table>
</div> </ContentArea>
); );
} }
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
DirectoryViewer.propTypes = { FolderViewer.propTypes = {
path: PropTypes.string.isRequired, path: PropTypes.string.isRequired,
details: PropTypes.objectOf( details: PropTypes.objectOf(
PropTypes.shape({ PropTypes.shape({

View File

@ -1,18 +1,27 @@
/** @jsx jsx */ /** @jsx jsx */
import { jsx } from '@emotion/core'; import { jsx } from '@emotion/core';
import { GoFileDirectory, GoFile } from 'react-icons/go'; import {
GoArrowBoth,
GoFile,
GoFileCode,
GoFileDirectory
} from 'react-icons/go';
import { FaTwitter, FaGithub } from 'react-icons/fa'; import { FaTwitter, FaGithub } from 'react-icons/fa';
function createIcon(Type, { css, ...rest }) { function createIcon(Type, { css, ...rest }) {
return <Type css={{ ...css, verticalAlign: 'text-bottom' }} {...rest} />; return <Type css={{ ...css, verticalAlign: 'text-bottom' }} {...rest} />;
} }
export function DirectoryIcon(props) { export function FileIcon(props) {
return createIcon(GoFileDirectory, props); return createIcon(GoFile, props);
} }
export function CodeFileIcon(props) { export function FileCodeIcon(props) {
return createIcon(GoFile, props); return createIcon(GoFileCode, props);
}
export function FolderIcon(props) {
return createIcon(GoFileDirectory, props);
} }
export function TwitterIcon(props) { export function TwitterIcon(props) {
@ -22,3 +31,7 @@ export function TwitterIcon(props) {
export function GitHubIcon(props) { export function GitHubIcon(props) {
return createIcon(FaGithub, props); return createIcon(FaGithub, props);
} }
export function ArrowBothIcon(props) {
return createIcon(GoArrowBoth, props);
}

View File

@ -1,11 +0,0 @@
import React, { createContext, useContext } from 'react';
const Context = createContext();
export function PackageInfoProvider({ children, ...rest }) {
return <Context.Provider children={children} value={rest} />;
}
export function usePackageInfo() {
return useContext(Context);
}

View File

@ -41,6 +41,9 @@ const globalStyles = css`
code { code {
${fontMono} ${fontMono}
font-size: 1rem;
padding: 0 3px;
background-color: #eee;
} }
dd, dd,
@ -58,9 +61,7 @@ function Link(props) {
css={{ css={{
color: '#0076ff', color: '#0076ff',
textDecoration: 'none', textDecoration: 'none',
':hover': { ':hover': { textDecoration: 'underline' }
textDecoration: 'underline'
}
}} }}
/> />
); );
@ -127,7 +128,10 @@ export default function App() {
css={{ css={{
textAlign: 'center', textAlign: 'center',
fontSize: '4.5em', fontSize: '4.5em',
letterSpacing: '0.05em' letterSpacing: '0.05em',
'@media (min-width: 700px)': {
margin: '1.5em 0 1em'
}
}} }}
> >
UNPKG UNPKG
@ -365,10 +369,11 @@ export default function App() {
</div> </div>
<p> <p>
The origin infrastructure runs on{' '} The origin servers run on world-class auto-scaling infrastructure
provided by{' '}
<Link href="https://cloud.google.com/">Google Cloud</Link> which <Link href="https://cloud.google.com/">Google Cloud</Link> which
automatically scales the number of available servers to meet the dynamically adjusts the number of available servers to meet the
current demand. current demand for maximum efficiency and uptime.
</p> </p>
<div <div