New "browse" UI
Also, separated out browse, ?meta, and ?module request handlers. Fixes #82
This commit is contained in:
356
modules/client/browse/App.js
Normal file
356
modules/client/browse/App.js
Normal file
@ -0,0 +1,356 @@
|
||||
/** @jsx jsx */
|
||||
import { Global, css, jsx } from '@emotion/core';
|
||||
import { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { fontSans, fontMono } from '../utils/style.js';
|
||||
|
||||
import { PackageInfoProvider } from './PackageInfo.js';
|
||||
import DirectoryViewer from './DirectoryViewer.js';
|
||||
import FileViewer from './FileViewer.js';
|
||||
import { TwitterIcon, GitHubIcon } from './Icons.js';
|
||||
|
||||
import SelectDownArrow from './images/SelectDownArrow.png';
|
||||
|
||||
const globalStyles = css`
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
*,
|
||||
*:before,
|
||||
*:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
${fontSans}
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
background: white;
|
||||
color: black;
|
||||
}
|
||||
|
||||
code {
|
||||
${fontMono}
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
select {
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
#root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
`;
|
||||
|
||||
// Adapted from https://github.com/highlightjs/highlight.js/blob/master/src/styles/atom-one-light.css
|
||||
const lightCodeStyles = css`
|
||||
.code-listing {
|
||||
background: #fbfdff;
|
||||
color: #383a42;
|
||||
}
|
||||
.code-comment,
|
||||
.code-quote {
|
||||
color: #a0a1a7;
|
||||
font-style: italic;
|
||||
}
|
||||
.code-doctag,
|
||||
.code-keyword,
|
||||
.code-link,
|
||||
.code-formula {
|
||||
color: #a626a4;
|
||||
}
|
||||
.code-section,
|
||||
.code-name,
|
||||
.code-selector-tag,
|
||||
.code-deletion,
|
||||
.code-subst {
|
||||
color: #e45649;
|
||||
}
|
||||
.code-literal {
|
||||
color: #0184bb;
|
||||
}
|
||||
.code-string,
|
||||
.code-regexp,
|
||||
.code-addition,
|
||||
.code-attribute,
|
||||
.code-meta-string {
|
||||
color: #50a14f;
|
||||
}
|
||||
.code-built_in,
|
||||
.code-class .code-title {
|
||||
color: #c18401;
|
||||
}
|
||||
.code-attr,
|
||||
.code-variable,
|
||||
.code-template-variable,
|
||||
.code-type,
|
||||
.code-selector-class,
|
||||
.code-selector-attr,
|
||||
.code-selector-pseudo,
|
||||
.code-number {
|
||||
color: #986801;
|
||||
}
|
||||
.code-symbol,
|
||||
.code-bullet,
|
||||
.code-meta,
|
||||
.code-selector-id,
|
||||
.code-title {
|
||||
color: #4078f2;
|
||||
}
|
||||
.code-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
.code-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
`;
|
||||
|
||||
const linkStyle = {
|
||||
color: '#0076ff',
|
||||
textDecoration: 'none',
|
||||
':hover': {
|
||||
textDecoration: 'underline'
|
||||
}
|
||||
};
|
||||
|
||||
export default function App({
|
||||
packageName,
|
||||
packageVersion,
|
||||
availableVersions = [],
|
||||
filename,
|
||||
target
|
||||
}) {
|
||||
function handleChange(event) {
|
||||
window.location.href = window.location.href.replace(
|
||||
'@' + packageVersion,
|
||||
'@' + event.target.value
|
||||
);
|
||||
}
|
||||
|
||||
const breadcrumbs = [];
|
||||
|
||||
if (filename === '/') {
|
||||
breadcrumbs.push(packageName);
|
||||
} else {
|
||||
let url = `/browse/${packageName}@${packageVersion}`;
|
||||
|
||||
breadcrumbs.push(
|
||||
<a href={`${url}/`} css={linkStyle}>
|
||||
{packageName}
|
||||
</a>
|
||||
);
|
||||
|
||||
const segments = filename
|
||||
.replace(/^\/+/, '')
|
||||
.replace(/\/+$/, '')
|
||||
.split('/');
|
||||
|
||||
const lastSegment = segments.pop();
|
||||
|
||||
segments.forEach(segment => {
|
||||
url += `/${segment}`;
|
||||
breadcrumbs.push(
|
||||
<a href={`${url}/`} css={linkStyle}>
|
||||
{segment}
|
||||
</a>
|
||||
);
|
||||
});
|
||||
|
||||
breadcrumbs.push(lastSegment);
|
||||
}
|
||||
|
||||
// TODO: Provide a user pref to go full width?
|
||||
const maxContentWidth = 940;
|
||||
|
||||
return (
|
||||
<PackageInfoProvider
|
||||
packageName={packageName}
|
||||
packageVersion={packageVersion}
|
||||
>
|
||||
<Fragment>
|
||||
<Global styles={globalStyles} />
|
||||
<Global styles={lightCodeStyles} />
|
||||
|
||||
<div css={{ flex: '1 0 auto' }}>
|
||||
<div
|
||||
css={{
|
||||
maxWidth: maxContentWidth,
|
||||
padding: '0 20px',
|
||||
margin: '0 auto'
|
||||
}}
|
||||
>
|
||||
<header css={{ textAlign: 'center' }}>
|
||||
<h1 css={{ fontSize: '3rem', marginTop: '2rem' }}>
|
||||
<a href="/" css={{ color: '#000', textDecoration: 'none' }}>
|
||||
UNPKG
|
||||
</a>
|
||||
</h1>
|
||||
{/*
|
||||
<nav>
|
||||
<a href="#" css={{ ...linkStyle, color: '#c400ff' }}>
|
||||
Become a Sponsor
|
||||
</a>
|
||||
</nav>
|
||||
*/}
|
||||
</header>
|
||||
|
||||
<header
|
||||
css={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between'
|
||||
}}
|
||||
>
|
||||
<h1 css={{ fontSize: '1.5rem' }}>
|
||||
<nav>
|
||||
{breadcrumbs.map((link, index) => (
|
||||
<span key={index}>
|
||||
{index !== 0 && (
|
||||
<span css={{ paddingLeft: 5, paddingRight: 5 }}>/</span>
|
||||
)}
|
||||
{link}
|
||||
</span>
|
||||
))}
|
||||
</nav>
|
||||
</h1>
|
||||
<div>
|
||||
<label htmlFor="version">Version:</label>{' '}
|
||||
<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>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div
|
||||
css={{
|
||||
maxWidth: maxContentWidth,
|
||||
padding: '0 20px',
|
||||
margin: '0 auto',
|
||||
'@media (max-width: 700px)': {
|
||||
padding: 0,
|
||||
margin: 0
|
||||
}
|
||||
}}
|
||||
>
|
||||
{target.type === 'directory' ? (
|
||||
<DirectoryViewer path={target.path} details={target.details} />
|
||||
) : target.type === 'file' ? (
|
||||
<FileViewer path={target.path} details={target.details} />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer
|
||||
css={{
|
||||
marginTop: '5rem',
|
||||
background: 'black',
|
||||
color: '#aaa'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
css={{
|
||||
maxWidth: maxContentWidth,
|
||||
padding: '10px 20px',
|
||||
margin: '0 auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between'
|
||||
}}
|
||||
>
|
||||
<p>© {new Date().getFullYear()} UNPKG</p>
|
||||
<p css={{ fontSize: '1.5rem' }}>
|
||||
<a
|
||||
title="Twitter"
|
||||
href="https://twitter.com/unpkg"
|
||||
css={{
|
||||
color: '#aaa',
|
||||
display: 'inline-block',
|
||||
':hover': { color: 'white' }
|
||||
}}
|
||||
>
|
||||
<TwitterIcon />
|
||||
</a>
|
||||
<a
|
||||
title="GitHub"
|
||||
href="https://github.com/mjackson/unpkg"
|
||||
css={{
|
||||
color: '#aaa',
|
||||
display: 'inline-block',
|
||||
marginLeft: '1rem',
|
||||
':hover': { color: 'white' }
|
||||
}}
|
||||
>
|
||||
<GitHubIcon />
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
</Fragment>
|
||||
</PackageInfoProvider>
|
||||
);
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
const targetType = PropTypes.shape({
|
||||
path: PropTypes.string.isRequired,
|
||||
type: PropTypes.oneOf(['directory', 'file']).isRequired,
|
||||
details: PropTypes.object.isRequired
|
||||
});
|
||||
|
||||
App.propTypes = {
|
||||
packageName: PropTypes.string.isRequired,
|
||||
packageVersion: PropTypes.string.isRequired,
|
||||
availableVersions: PropTypes.arrayOf(PropTypes.string),
|
||||
filename: PropTypes.string.isRequired,
|
||||
target: targetType.isRequired
|
||||
};
|
||||
}
|
182
modules/client/browse/DirectoryViewer.js
Normal file
182
modules/client/browse/DirectoryViewer.js
Normal file
@ -0,0 +1,182 @@
|
||||
/** @jsx jsx */
|
||||
import { jsx } from '@emotion/core';
|
||||
import PropTypes from 'prop-types';
|
||||
import VisuallyHidden from '@reach/visually-hidden';
|
||||
import sortBy from 'sort-by';
|
||||
|
||||
import { formatBytes } from '../utils/format.js';
|
||||
|
||||
import { DirectoryIcon, CodeFileIcon } from './Icons.js';
|
||||
|
||||
const linkStyle = {
|
||||
color: '#0076ff',
|
||||
textDecoration: 'none',
|
||||
':hover': {
|
||||
textDecoration: 'underline'
|
||||
}
|
||||
};
|
||||
|
||||
const tableCellStyle = {
|
||||
paddingTop: 6,
|
||||
paddingRight: 3,
|
||||
paddingBottom: 6,
|
||||
paddingLeft: 3,
|
||||
borderTop: '1px solid #eaecef'
|
||||
};
|
||||
|
||||
const iconCellStyle = {
|
||||
...tableCellStyle,
|
||||
color: '#424242',
|
||||
width: 17,
|
||||
paddingRight: 2,
|
||||
paddingLeft: 10,
|
||||
'@media (max-width: 700px)': {
|
||||
paddingLeft: 20
|
||||
}
|
||||
};
|
||||
|
||||
const typeCellStyle = {
|
||||
...tableCellStyle,
|
||||
textAlign: 'right',
|
||||
paddingRight: 10,
|
||||
'@media (max-width: 700px)': {
|
||||
paddingRight: 20
|
||||
}
|
||||
};
|
||||
|
||||
function getRelName(path, base) {
|
||||
return path.substr(base.length > 1 ? base.length + 1 : 1);
|
||||
}
|
||||
|
||||
export default function DirectoryViewer({ path, details: entries }) {
|
||||
const rows = [];
|
||||
|
||||
if (path !== '/') {
|
||||
rows.push(
|
||||
<tr key="..">
|
||||
<td css={iconCellStyle} />
|
||||
<td css={tableCellStyle}>
|
||||
<a title="Parent directory" href="../" css={linkStyle}>
|
||||
..
|
||||
</a>
|
||||
</td>
|
||||
<td css={tableCellStyle}></td>
|
||||
<td css={typeCellStyle}></td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
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')).forEach(({ path: dirname }) => {
|
||||
const relName = getRelName(dirname, path);
|
||||
const href = relName + '/';
|
||||
|
||||
rows.push(
|
||||
<tr key={relName}>
|
||||
<td css={iconCellStyle}>
|
||||
<DirectoryIcon />
|
||||
</td>
|
||||
<td css={tableCellStyle}>
|
||||
<a title={relName} href={href} css={linkStyle}>
|
||||
{relName}
|
||||
</a>
|
||||
</td>
|
||||
<td css={tableCellStyle}>-</td>
|
||||
<td css={typeCellStyle}>-</td>
|
||||
</tr>
|
||||
);
|
||||
});
|
||||
|
||||
files
|
||||
.sort(sortBy('path'))
|
||||
.forEach(({ path: filename, size, contentType }) => {
|
||||
const relName = getRelName(filename, path);
|
||||
const href = relName;
|
||||
|
||||
rows.push(
|
||||
<tr key={relName}>
|
||||
<td css={iconCellStyle}>
|
||||
<CodeFileIcon />
|
||||
</td>
|
||||
<td css={tableCellStyle}>
|
||||
<a title={relName} href={href} css={linkStyle}>
|
||||
{relName}
|
||||
</a>
|
||||
</td>
|
||||
<td css={tableCellStyle}>{formatBytes(size)}</td>
|
||||
<td css={typeCellStyle}>{contentType}</td>
|
||||
</tr>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
css={{
|
||||
border: '1px solid #dfe2e5',
|
||||
borderRadius: 3,
|
||||
borderTopWidth: 0,
|
||||
'@media (max-width: 700px)': {
|
||||
borderRightWidth: 0,
|
||||
borderLeftWidth: 0
|
||||
}
|
||||
}}
|
||||
>
|
||||
<table
|
||||
css={{
|
||||
width: '100%',
|
||||
borderCollapse: 'collapse',
|
||||
borderRadius: 2,
|
||||
background: '#fff'
|
||||
}}
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<VisuallyHidden>Icon</VisuallyHidden>
|
||||
</th>
|
||||
<th>
|
||||
<VisuallyHidden>Name</VisuallyHidden>
|
||||
</th>
|
||||
<th>
|
||||
<VisuallyHidden>Size</VisuallyHidden>
|
||||
</th>
|
||||
<th>
|
||||
<VisuallyHidden>Content Type</VisuallyHidden>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{rows}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
DirectoryViewer.propTypes = {
|
||||
path: PropTypes.string.isRequired,
|
||||
details: PropTypes.objectOf(
|
||||
PropTypes.shape({
|
||||
path: PropTypes.string.isRequired,
|
||||
type: PropTypes.oneOf(['directory', 'file']).isRequired,
|
||||
contentType: PropTypes.string, // file only
|
||||
integrity: PropTypes.string, // file only
|
||||
size: PropTypes.number // file only
|
||||
})
|
||||
).isRequired
|
||||
};
|
||||
}
|
212
modules/client/browse/FileViewer.js
Normal file
212
modules/client/browse/FileViewer.js
Normal file
@ -0,0 +1,212 @@
|
||||
/** @jsx jsx */
|
||||
import { jsx } from '@emotion/core';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { formatBytes } from '../utils/format.js';
|
||||
import { createHTML } from '../utils/markup.js';
|
||||
|
||||
import { usePackageInfo } from './PackageInfo.js';
|
||||
|
||||
function getBasename(path) {
|
||||
const segments = path.split('/');
|
||||
return segments[segments.length - 1];
|
||||
}
|
||||
|
||||
function ImageViewer({ path, uri }) {
|
||||
return (
|
||||
<div css={{ padding: 20, textAlign: 'center' }}>
|
||||
<img title={getBasename(path)} src={uri} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CodeListing({ highlights }) {
|
||||
const lines = highlights.slice(0);
|
||||
const hasTrailingNewline = lines.length && lines[lines.length - 1] === '';
|
||||
if (hasTrailingNewline) {
|
||||
lines.pop();
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="code-listing"
|
||||
css={{
|
||||
overflowX: 'auto',
|
||||
overflowY: 'hidden',
|
||||
paddingTop: 5,
|
||||
paddingBottom: 5
|
||||
}}
|
||||
>
|
||||
<table
|
||||
css={{
|
||||
border: 'none',
|
||||
borderCollapse: 'collapse',
|
||||
borderSpacing: 0
|
||||
}}
|
||||
>
|
||||
<tbody>
|
||||
{lines.map((line, index) => {
|
||||
const lineNumber = index + 1;
|
||||
|
||||
return (
|
||||
<tr key={index}>
|
||||
<td
|
||||
id={`L${lineNumber}`}
|
||||
css={{
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
color: 'rgba(27,31,35,.3)',
|
||||
textAlign: 'right',
|
||||
verticalAlign: 'top',
|
||||
width: '1%',
|
||||
minWidth: 50,
|
||||
userSelect: 'none'
|
||||
}}
|
||||
>
|
||||
<span>{lineNumber}</span>
|
||||
</td>
|
||||
<td
|
||||
id={`LC${lineNumber}`}
|
||||
css={{
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
color: '#24292e',
|
||||
whiteSpace: 'pre'
|
||||
}}
|
||||
>
|
||||
<code dangerouslySetInnerHTML={createHTML(line)} />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
{!hasTrailingNewline && (
|
||||
<tr key="no-newline">
|
||||
<td
|
||||
css={{
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
color: 'rgba(27,31,35,.3)',
|
||||
textAlign: 'right',
|
||||
verticalAlign: 'top',
|
||||
width: '1%',
|
||||
minWidth: 50,
|
||||
userSelect: 'none'
|
||||
}}
|
||||
>
|
||||
\
|
||||
</td>
|
||||
<td
|
||||
css={{
|
||||
paddingLeft: 10,
|
||||
color: 'rgba(27,31,35,.3)',
|
||||
userSelect: 'none'
|
||||
}}
|
||||
>
|
||||
No newline at end of file
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BinaryViewer() {
|
||||
return (
|
||||
<div css={{ padding: 20 }}>
|
||||
<p css={{ textAlign: 'center' }}>No preview available.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function FileViewer({ path, details }) {
|
||||
const { packageName, packageVersion } = usePackageInfo();
|
||||
const { highlights, uri, language, size } = details;
|
||||
|
||||
const segments = path.split('/');
|
||||
const filename = segments[segments.length - 1];
|
||||
|
||||
return (
|
||||
<div
|
||||
css={{
|
||||
border: '1px solid #dfe2e5',
|
||||
borderRadius: 3,
|
||||
'@media (max-width: 700px)': {
|
||||
borderRightWidth: 0,
|
||||
borderLeftWidth: 0
|
||||
}
|
||||
}}
|
||||
>
|
||||
<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: 700px)': {
|
||||
paddingRight: 20,
|
||||
paddingLeft: 20
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span>{formatBytes(size)}</span> <span>{language}</span>{' '}
|
||||
<a
|
||||
title={filename}
|
||||
href={`/${packageName}@${packageVersion}${path}`}
|
||||
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 ? (
|
||||
<CodeListing highlights={highlights} />
|
||||
) : uri ? (
|
||||
<ImageViewer path={path} uri={uri} />
|
||||
) : (
|
||||
<BinaryViewer />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
FileViewer.propTypes = {
|
||||
path: PropTypes.string.isRequired,
|
||||
details: PropTypes.shape({
|
||||
contentType: PropTypes.string.isRequired,
|
||||
highlights: PropTypes.arrayOf(PropTypes.string), // code
|
||||
uri: PropTypes.string, // images
|
||||
integrity: PropTypes.string.isRequired,
|
||||
language: PropTypes.string.isRequired,
|
||||
size: PropTypes.number.isRequired
|
||||
}).isRequired
|
||||
};
|
||||
}
|
24
modules/client/browse/Icons.js
Normal file
24
modules/client/browse/Icons.js
Normal file
@ -0,0 +1,24 @@
|
||||
/** @jsx jsx */
|
||||
import { jsx } from '@emotion/core';
|
||||
import { GoFileDirectory, GoFile } from 'react-icons/go';
|
||||
import { FaTwitter, FaGithub } from 'react-icons/fa';
|
||||
|
||||
function createIcon(Type, { css, ...rest }) {
|
||||
return <Type css={{ ...css, verticalAlign: 'text-bottom' }} {...rest} />;
|
||||
}
|
||||
|
||||
export function DirectoryIcon(props) {
|
||||
return createIcon(GoFileDirectory, props);
|
||||
}
|
||||
|
||||
export function CodeFileIcon(props) {
|
||||
return createIcon(GoFile, props);
|
||||
}
|
||||
|
||||
export function TwitterIcon(props) {
|
||||
return createIcon(FaTwitter, props);
|
||||
}
|
||||
|
||||
export function GitHubIcon(props) {
|
||||
return createIcon(FaGithub, props);
|
||||
}
|
11
modules/client/browse/PackageInfo.js
Normal file
11
modules/client/browse/PackageInfo.js
Normal file
@ -0,0 +1,11 @@
|
||||
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);
|
||||
}
|
BIN
modules/client/browse/images/DownArrow.png
Normal file
BIN
modules/client/browse/images/DownArrow.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 307 B |
BIN
modules/client/browse/images/SelectDownArrow.png
Normal file
BIN
modules/client/browse/images/SelectDownArrow.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 343 B |
Reference in New Issue
Block a user