Merge branch 'feature/primer' into develop

This commit is contained in:
EnixCoda 2020-03-25 01:02:38 +08:00
commit 0fbb9bd217
No known key found for this signature in database
GPG key ID: 0C1A07377913A1DD
25 changed files with 1483 additions and 484 deletions

View file

@ -3,8 +3,9 @@
[
"@babel/preset-env",
{
"modules": false,
"targets": {
"chrome": "67"
"esmodules": true
}
}
],

View file

@ -11,14 +11,18 @@
"start": "VERSION=dev-v$(node scripts/get-version.js) webpack --watch",
"debug-firefox": "web-ext run -s dist",
"analyse-bundle": "ANALYSE= NODE_ENV=production webpack",
"postinstall": "rm -rf node_modules/@types/react-native",
"build": "VERSION=v$(node scripts/get-version.js) NODE_ENV=production webpack",
"roll": "make release"
},
"dependencies": {
"@primer/components": "^16.0.0",
"@primer/css": "^14.2.0",
"@primer/octicons": "^9.2.0",
"@primer/octicons-react": "^9.2.0",
"@sentry/browser": "^5.12.1",
"@types/firefox-webext-browser": "^70.0.1",
"@types/history": "^4.7.5",
"@types/ini": "^1.3.30",
"@types/js-base64": "^2.3.1",
"@types/node": "^11.10.4",
@ -26,13 +30,15 @@
"@types/react": "^16.8.24",
"@types/react-dom": "^16.8.5",
"@types/react-window": "^1.8.1",
"@types/styled-components": "^5.0.1",
"ini": "^1.3.5",
"js-base64": "^2.5.1",
"nprogress": "^0.2.0",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react": "^16.13.0",
"react-dom": "^16.13.0",
"react-use": "^13.8.0",
"react-window": "^1.8.5",
"styled-components": "^5.0.1",
"webextension-polyfill": "^0.5.0"
},
"devDependencies": {
@ -53,10 +59,10 @@
"fork-ts-checker-webpack-plugin": "^0.5.2",
"fsevents": "^1.2.4",
"json-loader": "^0.5.7",
"less": "^3.9.0",
"less-loader": "^4.0.5",
"mini-css-extract-plugin": "^0.9.0",
"raw-loader": "^4.0.0",
"sass": "^1.26.2",
"sass-loader": "^8.0.2",
"typescript": "^3.7.2",
"uglifyjs-webpack-plugin": "^2.1.2",
"url-loader": "^1.1.2",

View file

@ -1,3 +1,4 @@
import { Text } from '@primer/components'
import { LoadingIndicator } from 'components/LoadingIndicator'
import { Node } from 'components/Node'
import { SearchBar } from 'components/SearchBar'
@ -6,11 +7,10 @@ import { connect } from 'driver/connect'
import { FileExplorerCore } from 'driver/core'
import { ConnectorState, Props } from 'driver/core/FileExplorer'
import * as React from 'react'
import useEvent from 'react-use/esm/useEvent'
import { useEvent, usePrevious } from 'react-use'
import { FixedSizeList as List, ListChildComponentProps, ListProps } from 'react-window'
import { cx } from 'utils/cx'
import { useOnLocationChange } from 'utils/hooks/useOnLocationChange'
import { usePrevious } from 'utils/hooks/usePrevious'
import { getCurrentPath } from 'utils/URLHelper'
import { TreeNode, VisibleNodes } from 'utils/VisibleNodesGenerator'
import { Icon } from './Icon'
@ -69,7 +69,11 @@ const RawFileExplorer: React.FC<Props & ConnectorState> = function RawFileExplor
({ nodes, focusedNode }: VisibleNodes) => {
const inSearch = searchKey !== ''
if (inSearch && nodes.length === 0) {
return <label className={'no-results'}>No results found.</label>
return (
<Text marginTop={6} textAlign="center" color="text.gray">
No results found.
</Text>
)
}
return (
<SizeObserver className={'files'}>
@ -210,7 +214,7 @@ function ListView({
}}
itemData={{ nodes }}
itemCount={nodes.length}
itemSize={35}
itemSize={36}
height={height}
width={width}
>

View file

@ -1,3 +1,4 @@
import { Breadcrumb } from '@primer/components'
import * as React from 'react'
import { MetaData } from 'utils/GitHubHelper'
@ -10,15 +11,13 @@ export function MetaBar({ metaData }: Props) {
const repoUrl = metaData?.api?.html_url
return (
<div className={'meta-bar'}>
<a className={'username'} href={userUrl}>
{metaData.userName}
</a>
&nbsp;/&nbsp;
<a className={'repo-name'} href={repoUrl}>
{metaData.repoName}
</a>
&nbsp;/&nbsp;
{metaData.branchName}
<Breadcrumb>
<Breadcrumb.Item href={userUrl}>{metaData.userName}</Breadcrumb.Item>
<Breadcrumb.Item className={'repo-name'} href={repoUrl}>
{metaData.repoName}
</Breadcrumb.Item>
<Breadcrumb.Item selected>{metaData.branchName}</Breadcrumb.Item>
</Breadcrumb>
</div>
)
}

View file

@ -1,3 +1,4 @@
import { Text } from '@primer/components'
import { useConfigs } from 'containers/ConfigsContext'
import * as React from 'react'
import { cx } from 'utils/cx'
@ -45,24 +46,19 @@ export function Node({ node, depth, expanded, focused, renderActions, style, onC
const { name, path } = node
return (
<div
className={cx(`node-item-row`, { focused, disabled: node.accessDenied })}
style={style}
<a
href={node.url}
onClick={onClickNode}
className={cx(`node-item`, { focused, disabled: node.accessDenied, expanded })}
style={{ ...style, paddingLeft: `${10 + 20 * depth}px` }}
title={path}
>
<a href={node.url} onClick={onClickNode}>
<div
className={cx('node-item', { expanded })}
style={{ paddingLeft: `${10 + 20 * depth}px` }}
>
<div className={'node-item-label'}>
<NodeItemIcon node={node} open={expanded} />
<span className={'node-item-name'}>{name}</span>
</div>
{renderActions && <div>{renderActions(node)}</div>}
</div>
</a>
</div>
<div className={'node-item-label'}>
<NodeItemIcon node={node} open={expanded} />
<Text fontSize={14}>{name}</Text>
</div>
{renderActions && <div>{renderActions(node)}</div>}
</a>
)
}

View file

@ -1,11 +1,11 @@
import { HorizontalResizeHandler } from 'components/ResizeHandler'
import { useConfigs } from 'containers/ConfigsContext'
import * as React from 'react'
import { useWindowSize } from 'react-use'
import { cx } from 'utils/cx'
import { bodySpacingClassName } from 'utils/DOMHelper'
import * as features from 'utils/features'
import { useMediaStyleSheet } from 'utils/hooks/useMediaStyleSheet'
import { useWindowSize } from 'utils/hooks/useWindowSize'
export type Size = number
type Props = {
@ -24,13 +24,11 @@ export function Resizable({ baseSize, className, children }: React.PropsWithChil
setSize(baseSize)
}, [baseSize])
useWindowSize(
width => {
if (size > width - MINIMAL_CONTENT_VIEWPORT_WIDTH)
setSize(width - MINIMAL_CONTENT_VIEWPORT_WIDTH)
},
[size],
)
const { width } = useWindowSize()
React.useEffect(() => {
if (size > width - MINIMAL_CONTENT_VIEWPORT_WIDTH)
setSize(width - MINIMAL_CONTENT_VIEWPORT_WIDTH)
}, [width, size])
React.useEffect(() => {
document.documentElement.style.setProperty('--gitako-width', size + 'px')
@ -49,17 +47,13 @@ export function Resizable({ baseSize, className, children }: React.PropsWithChil
size,
)
const onResize = React.useCallback((size: number) => {
if (size < window.innerWidth - MINIMAL_CONTENT_VIEWPORT_WIDTH) setSize(size)
}, [])
return (
<div className={cx('gitako-position-wrapper', className)}>
<div className={'gitako-position-content'}>{children}</div>
{features.resize && (
<HorizontalResizeHandler
onResize={size => {
if (size < window.innerWidth - MINIMAL_CONTENT_VIEWPORT_WIDTH) setSize(size)
}}
size={size}
/>
)}
{features.resize && <HorizontalResizeHandler onResize={onResize} size={size} />}
</div>
)
}

View file

@ -1,3 +1,5 @@
import { TextInput } from '@primer/components'
import { Search } from '@primer/octicons-react'
import * as React from 'react'
import { cx } from 'utils/cx'
@ -10,10 +12,12 @@ type Props = {
export function SearchBar({ onSearch, onFocus, searchKey }: Props) {
return (
<div className={'search-input-wrapper'}>
<input
<TextInput
backgroundColor="white"
icon={Search as any}
onFocus={onFocus}
tabIndex={0}
className={cx('form-control', 'search-input', {
className={cx('search-input', {
error: !isValidRegexpSource(searchKey),
})}
aria-label="search files"

View file

@ -1,3 +1,4 @@
import { Link } from '@primer/components'
import { Icon } from 'components/Icon'
import { VERSION } from 'env'
import * as React from 'react'
@ -5,6 +6,7 @@ import { Config } from 'utils/configHelper'
import { useStates } from 'utils/hooks/useStates'
import { AccessTokenSettings } from './settings/AccessTokenSettings'
import { FileTreeSettings } from './settings/FileTreeSettings'
import { SettingsSection } from './settings/SettingsSection'
import { SidebarSettings } from './settings/SidebarSettings'
import { SimpleToggleField } from './SimpleToggleField'
@ -52,14 +54,13 @@ function SettingsBarContent() {
return (
<>
<h3 className={'gitako-settings-bar-title'}>Settings</h3>
<h2 className={'gitako-settings-bar-title'}>Settings</h2>
<div className={'gitako-settings-bar-content'}>
<div className={'shadow-shelter'} />
<AccessTokenSettings />
<SidebarSettings />
<FileTreeSettings />
<div className={'gitako-settings-bar-content-section others'}>
<h4>More</h4>
<SettingsSection title={'More'}>
{moreFields.map(field => (
<React.Fragment key={field.key}>
<SimpleToggleField field={field} />
@ -67,13 +68,12 @@ function SettingsBarContent() {
))}
{reloadHint && <div className={'hint'}>{reloadHint}</div>}
</div>
<div className={'gitako-settings-bar-content-section issue'}>
<h4>Contact</h4>
</SettingsSection>
<SettingsSection title={'Contact'}>
<a href="https://github.com/EnixCoda/Gitako/issues" target="_blank">
Report bug / Request feature.
</a>
</div>
</SettingsSection>
</div>
</>
)
@ -85,14 +85,15 @@ export function SettingsBar(props: Props) {
<div className={'gitako-settings-bar'}>
{activated && <SettingsBarContent />}
<div className={'header-row'}>
<a
className={'version'}
<Link
color="gray.4"
fontSize={14}
href={wikiLinks.changeLog}
target={'_blank'}
title={'Check out new features!'}
>
{VERSION}
</a>
</Link>
{activated ? (
<Icon
type={'chevron-down'}

View file

@ -11,11 +11,10 @@ import { SideBarCore } from 'driver/core'
import { ConnectorState, Props } from 'driver/core/SideBar'
import { oauth } from 'env'
import * as React from 'react'
import useEvent from 'react-use/esm/useEvent'
import { useEvent, useUpdateEffect } from 'react-use'
import { cx } from 'utils/cx'
import * as DOMHelper from 'utils/DOMHelper'
import { JSONRequest, parseURLSearch } from 'utils/general'
import { useDidUpdate } from 'utils/hooks/useDidUpdate'
import * as keyHelper from 'utils/keyHelper'
import * as URLHelper from 'utils/URLHelper'
@ -83,7 +82,7 @@ const RawGitako: React.FC<Props & ConnectorState> = function RawGitako(props) {
useEvent('pjax:complete', attachCopySnippetButton, window)
// init again when setting new accessToken
useDidUpdate(() => {
useUpdateEffect(() => {
props.init()
}, [accessToken || '']) // fallback for preventing duplicated requests
@ -162,11 +161,11 @@ function renderAccessDeniedError(hasToken: boolean) {
) : (
<p>
Gitako needs access token to read this project due to{' '}
<a target="_blank" href="https://developer.github.com/v3/#rate-limiting">
<a href="https://developer.github.com/v3/#rate-limiting" target="_blank">
GitHub rate limiting
</a>{' '}
and{' '}
<a target="_blank" href="https://developer.github.com/v3/#authentication">
<a href="https://developer.github.com/v3/#authentication" target="_blank">
auth needs
</a>
. Please setup access token in the settings panel below.

View file

@ -1,5 +1,6 @@
import { useConfigs } from 'containers/ConfigsContext'
import * as React from 'react'
import { Field } from './settings/Field'
import { SimpleField } from './SettingsBar'
type Props = {
@ -12,9 +13,24 @@ export function SimpleToggleField({ field, onChange }: Props) {
const configContext = useConfigs()
const value = configContext.val[field.key]
return (
<div className={'form-checkbox'}>
<Field
id={field.key}
title={
<>
{field.label}{' '}
{field.wikiLink ? (
<a href={field.wikiLink} target={'_blank'}>
(?)
</a>
) : (
field.description && <p className={'note'}>{field.description}</p>
)}
</>
}
className={'field-checkbox'}
checkbox
>
<input
className={'form-control'}
id={field.key}
name={field.key}
type={'checkbox'}
@ -25,16 +41,6 @@ export function SimpleToggleField({ field, onChange }: Props) {
}}
checked={overwrite ? overwrite.value(value) : Boolean(value)}
/>
<label htmlFor={field.key}>
{field.label}{' '}
{field.wikiLink ? (
<a href={field.wikiLink} target={'_blank'}>
(?)
</a>
) : (
field.description && <p className={'note'}>{field.description}</p>
)}
</label>
</div>
</Field>
)
}

View file

@ -29,7 +29,7 @@ export function SizeObserver({ type = 'div', children, ...rest }: Props) {
)
}, [])
React.useLayoutEffect(() => {
React.useEffect(() => {
if (features.resize) {
const observer = new window.ResizeObserver(entries => {
const entry = entries[0]

View file

@ -1,8 +1,10 @@
import { Button, TextInput } from '@primer/components'
import { wikiLinks } from 'components/SettingsBar'
import { useConfigs } from 'containers/ConfigsContext'
import { oauth } from 'env'
import * as React from 'react'
import { useStates } from 'utils/hooks/useStates'
import { SettingsSection } from './SettingsSection'
const ACCESS_TOKEN_REGEXP = /^[0-9a-f]{40}$/
@ -13,6 +15,7 @@ export function AccessTokenSettings(props: React.PropsWithChildren<Props>) {
const hasAccessToken = Boolean(configContext.val.access_token)
const useAccessToken = useStates('')
const useAccessTokenHint = useStates<React.ReactNode>('')
const focusInput = useStates(false)
const { val: accessTokenHint } = useAccessTokenHint
const { val: accessToken } = useAccessToken
@ -26,7 +29,7 @@ export function AccessTokenSettings(props: React.PropsWithChildren<Props>) {
({ currentTarget: { value } }: React.FormEvent<HTMLInputElement>) => {
useAccessToken.set(value)
useAccessTokenHint.set(
ACCESS_TOKEN_REGEXP.test(value) ? '' : 'This token is in unknown format.',
ACCESS_TOKEN_REGEXP.test(value) ? '' : 'Gitako does not recognize the token.',
)
},
[],
@ -57,47 +60,51 @@ export function AccessTokenSettings(props: React.PropsWithChildren<Props>) {
)
return (
<div className={'gitako-settings-bar-content-section access-token'}>
<h4>
Access Token{' '}
<a href={wikiLinks.createAccessToken} target="_blank">
(?)
</a>
</h4>
{!hasAccessToken && (
<a
className={'link-button'}
onClick={() => {
// use js here to make sure redirect_uri is latest url
const url = `https://github.com/login/oauth/authorize?client_id=${
oauth.clientId
}&scope=repo&redirect_uri=${encodeURIComponent(window.location.href)}`
window.location.href = url
}}
>
Create with OAuth (recommended)
</a>
<SettingsSection
title={
<>
Access Token{' '}
<a href={wikiLinks.createAccessToken} target="_blank">
(?)
</a>
</>
}
>
{hasAccessToken ? (
<Button onClick={() => configContext.set({ access_token: '' })}>Clear</Button>
) : (
<div>
<a
className={'link-button'}
onClick={() => {
// use js here to make sure redirect_uri is latest url
const url = `https://github.com/login/oauth/authorize?client_id=${
oauth.clientId
}&scope=repo&redirect_uri=${encodeURIComponent(window.location.href)}`
window.location.href = url
}}
>
Create with OAuth (recommended)
</a>
<div className={'access-token-input-control'}>
<TextInput
backgroundColor="#fff"
marginRight={1}
className={'access-token-input'}
value={accessToken}
placeholder="Or input here manually"
onFocus={() => focusInput.set(true)}
onBlur={() => focusInput.set(false)}
onChange={onInputAccessToken}
onKeyPress={onPressAccessToken}
/>
<Button onClick={() => saveToken()} disabled={!accessToken}>
Save
</Button>
</div>
</div>
)}
<div className={'access-token-input-control'}>
<input
className={'access-token-input form-control'}
disabled={hasAccessToken}
placeholder={hasAccessToken ? 'Your token is saved' : 'Or input here manually'}
value={accessToken}
onChange={onInputAccessToken}
onKeyPress={onPressAccessToken}
/>
{hasAccessToken && !accessToken ? (
<button className={'btn'} onClick={() => configContext.set({ access_token: '' })}>
Clear
</button>
) : (
<button className={'btn'} onClick={() => saveToken()} disabled={!accessToken}>
Save
</button>
)}
</div>
{accessTokenHint && <span className={'hint'}>{accessTokenHint}</span>}
</div>
{accessTokenHint && !focusInput.val && <span className={'hint'}>{accessTokenHint}</span>}
</SettingsSection>
)
}

View file

@ -0,0 +1,33 @@
import * as React from 'react'
import { cx } from 'utils/cx'
type Props = {
id?: string
className?: string
checkbox?: boolean
title: React.ReactNode
}
export function Field({
className,
title,
id,
checkbox,
children,
}: React.PropsWithChildren<Props>) {
return (
<div className={cx('field', className)}>
{checkbox ? (
<>
{children}
<label htmlFor={id}>{title}</label>
</>
) : (
<>
<label htmlFor={id}>{title}</label>
<div>{children}</div>
</>
)}
</div>
)
}

View file

@ -3,6 +3,8 @@ import { SimpleToggleField } from 'components/SimpleToggleField'
import { useConfigs } from 'containers/ConfigsContext'
import * as React from 'react'
import { Config } from 'utils/configHelper'
import { Field } from './Field'
import { SettingsSection } from './SettingsSection'
const options: {
key: Config['icons']
@ -31,12 +33,8 @@ type Props = {}
export function FileTreeSettings(props: React.PropsWithChildren<Props>) {
const configContext = useConfigs()
return (
<div className={'gitako-settings-bar-content-section'}>
<h4>File Tree</h4>
<label className="form-label" htmlFor="file-tree-icons">
Icons
</label>
<div>
<SettingsSection title={'File Tree'}>
<Field title="Icons" id="file-tree-icons">
<select
id="file-tree-icons"
onChange={e => {
@ -44,8 +42,6 @@ export function FileTreeSettings(props: React.PropsWithChildren<Props>) {
icons: e.target.value as Config['icons'],
})
}}
className={'file-tree-input form-control'}
placeholder={'focus here and press the shortcut keys'}
value={configContext.val.icons}
>
{options.map(option => (
@ -54,7 +50,7 @@ export function FileTreeSettings(props: React.PropsWithChildren<Props>) {
</option>
))}
</select>
</div>
</Field>
<SimpleToggleField
field={{
key: 'compressSingletonFolder',
@ -62,6 +58,6 @@ export function FileTreeSettings(props: React.PropsWithChildren<Props>) {
wikiLink: wikiLinks.compressSingletonFolder,
}}
/>
</div>
</SettingsSection>
)
}

View file

@ -0,0 +1,13 @@
import * as React from 'react'
type Props = {
title?: React.ReactNode
}
export function SettingsSection({ title, children }: React.PropsWithChildren<Props>) {
return (
<div className={'settings-section'}>
{title && <h3>{title}</h3>}
{children}
</div>
)
}

View file

@ -1,9 +1,12 @@
import { Button, TextInput } from '@primer/components'
import { SimpleToggleField } from 'components/SimpleToggleField'
import { useConfigs } from 'containers/ConfigsContext'
import * as React from 'react'
import { friendlyFormatShortcut } from 'utils/general'
import { useStates } from 'utils/hooks/useStates'
import * as keyHelper from 'utils/keyHelper'
import { Field } from './Field'
import { SettingsSection } from './SettingsSection'
type Props = {}
@ -11,44 +14,56 @@ export function SidebarSettings(props: React.PropsWithChildren<Props>) {
const configContext = useConfigs()
const useToggleShowSideBarShortcut = useStates(configContext.val.shortcut)
const { val: toggleShowSideBarShortcut } = useToggleShowSideBarShortcut
const focused = useStates(false)
React.useEffect(() => {
useToggleShowSideBarShortcut.set(configContext.val.shortcut)
}, [configContext.val.shortcut])
return (
<div className={'gitako-settings-bar-content-section toggle-shortcut'}>
<h4>Toggle Sidebar</h4>
<label className="form-label" htmlFor="toggle-sidebar-shortcut">
Keyboard Shortcut
</label>
<div className={'toggle-shortcut-input-control'}>
<input
id="toggle-sidebar-shortcut"
className={'toggle-shortcut-input form-control'}
placeholder={'focus here and press the shortcut keys'}
value={friendlyFormatShortcut(toggleShowSideBarShortcut)}
onKeyDown={React.useCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
e.preventDefault()
e.stopPropagation()
// Clear shortcut with backspace
const shortcut = e.key === 'Backspace' ? '' : keyHelper.parseEvent(e)
useToggleShowSideBarShortcut.set(shortcut)
}, [])}
readOnly
/>
<button
className={'btn'}
disabled={toggleShowSideBarShortcut === configContext.val.shortcut}
onClick={() => {
const { val: toggleShowSideBarShortcut } = useToggleShowSideBarShortcut
if (typeof toggleShowSideBarShortcut !== 'string') return
configContext.set({ shortcut: toggleShowSideBarShortcut })
}}
>
Save
</button>
</div>
<SettingsSection title={'Toggle Sidebar'}>
<Field id="toggle-sidebar-shortcut" title="Keyboard Shortcut">
<div className={'toggle-shortcut-input-control'}>
<TextInput
id="toggle-sidebar-shortcut"
backgroundColor="#fff"
marginRight={1}
className={'toggle-shortcut-input'}
onFocus={() => focused.set(true)}
onBlur={() => focused.set(false)}
placeholder={focused.val ? 'Press key combination' : 'Click here to set'}
value={friendlyFormatShortcut(toggleShowSideBarShortcut)}
onKeyDown={React.useCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
e.preventDefault()
e.stopPropagation()
// Clear shortcut with backspace
const shortcut = e.key === 'Backspace' ? '' : keyHelper.parseEvent(e)
useToggleShowSideBarShortcut.set(shortcut)
}, [])}
readOnly
/>
{configContext.val.shortcut === toggleShowSideBarShortcut ? (
<Button
disabled={!configContext.val.shortcut}
onClick={() => {
configContext.set({ shortcut: '' })
}}
>
Clear
</Button>
) : (
<Button
onClick={() => {
const { val: toggleShowSideBarShortcut } = useToggleShowSideBarShortcut
if (typeof toggleShowSideBarShortcut !== 'string') return
configContext.set({ shortcut: toggleShowSideBarShortcut })
}}
>
Save
</Button>
)}
</div>
</Field>
<SimpleToggleField
field={{
key: 'intelligentToggle',
@ -59,6 +74,6 @@ export function SidebarSettings(props: React.PropsWithChildren<Props>) {
},
}}
/>
</div>
</SettingsSection>
)
}

View file

@ -1,23 +1,31 @@
@import '~nprogress/nprogress.css';
@name: gitako;
@import '~@primer/css/base/index.scss';
@min-screen-width: 1280px;
@github-content-width: 1020px;
@side-bar-base-width: (@min-screen-width - @github-content-width);
@min-width-with-gitako: (@min-screen-width + @side-bar-base-width);
@import '~@primer/css/support/variables/colors.scss';
@import '~@primer/css/support/variables/color-system.scss';
@import '~@primer/css/support/variables/typography.scss';
@import '~@primer/css/support/variables/misc.scss';
@import '~@primer/css/support/variables/layout.scss';
@github-header-z-index: 32;
@github-pull-request-float-header-z-index: 110;
@minimal-z-index: max(@github-header-z-index, @github-pull-request-float-header-z-index) + 1;
$name: gitako;
$min-screen-width: 1280px;
$github-content-width: 1020px;
$side-bar-base-width: $min-screen-width - $github-content-width;
$min-width-with-gitako: $min-screen-width + $side-bar-base-width;
$github-header-z-index: 32;
$github-pull-request-float-header-z-index: 110;
$minimal-z-index: max($github-header-z-index, $github-pull-request-float-header-z-index) + 1;
:root {
--gitako-width: @side-bar-base-width;
--gitako-width: #{$side-bar-base-width};
}
.@{name}-ready {
.#{$name}-ready {
.js-header-wrapper {
background: #24292e;
background: $gray-dark;
}
.Header {
max-width: 1012px;
@ -25,9 +33,9 @@
}
}
@media (min-width: @github-content-width) {
@media (min-width: $github-content-width) {
body {
min-width: @github-content-width;
min-width: $github-content-width;
}
}
@ -37,20 +45,20 @@
width: 0;
height: 0;
top: 8px;
left: ~'calc(100% - 40px)';
left: calc(100% - 40px);
z-index: 1;
.clippy {
width: 32px;
height: 32px;
border: 1px solid #ddd;
border: 1px solid $border-gray-dark;
border-radius: 4px;
background: #f9f9f9;
background: $bg-gray-light;
&:hover {
background: #fff;
background: $bg-white;
}
&:active {
background: #f0f0f0;
background: $bg-gray-light;
}
.icon {
width: 100%;
@ -74,7 +82,7 @@
left: 0; /* reposition progress bar of GitHub */
}
.hidden {
%hidden {
display: none;
}
@ -82,12 +90,12 @@
visibility: hidden;
}
.@{name}-toggle-show-button-wrapper {
@animation-duration: 0.5s;
.#{$name}-toggle-show-button-wrapper {
$animation-duration: 0.5s;
position: fixed;
top: 5px;
left: 0;
z-index: @minimal-z-index;
z-index: $minimal-z-index;
display: inline-flex;
justify-content: center;
align-items: center;
@ -96,53 +104,53 @@
will-change: transform;
border: 1px solid transparent;
.transform-expanded {
@mixin transform-expanded() {
transform: translate(calc(var(--gitako-width) - 30px));
}
.transform-collapsed {
@mixin transform-collapsed() {
transform: translate(5px);
}
.transform-expanded;
animation: toggle-show-button-wrapper-expand @animation-duration;
@include transform-expanded();
animation: toggle-show-button-wrapper-expand $animation-duration;
&.collapsed {
border-color: #999999;
border-color: $border-gray;
border-radius: 3px;
background: #f5f5f5;
background: $bg-gray;
.transform-collapsed;
animation: toggle-show-button-wrapper-collapse @animation-duration;
@include transform-collapsed();
animation: toggle-show-button-wrapper-collapse $animation-duration;
.action-icon {
color: #0366d6d0;
color: $blue;
}
}
@keyframes toggle-show-button-wrapper-expand {
from {
.transform-collapsed;
@include transform-collapsed();
}
to {
.transform-expanded;
@include transform-expanded();
}
}
@keyframes toggle-show-button-wrapper-collapse {
from {
.transform-expanded;
@include transform-expanded();
}
to {
.transform-collapsed;
@include transform-collapsed();
}
}
.action-icon {
color: #666666;
color: $gray;
width: 20px;
height: 20px;
text-align: center;
transition: all @animation-duration ease;
transition: all $animation-duration ease;
.octicon {
width: 100%;
height: 100%;
@ -151,7 +159,7 @@
&.error {
.action-icon {
color: #d73a49;
color: $red;
}
}
@ -168,36 +176,36 @@
}
}
.@{name}-side-bar {
.@{name}-position-wrapper {
.#{$name}-side-bar {
.#{$name}-position-wrapper {
position: fixed;
top: 0;
left: 0;
height: 100vh;
z-index: @minimal-z-index;
z-index: $minimal-z-index;
display: flex;
&.hidden {
.hidden;
@extend %hidden;
}
.gitako-position-content {
width: var(--gitako-width);
}
}
.@{name}-resize-handler {
.#{$name}-resize-handler {
display: flex;
align-items: center;
cursor: ew-resize;
user-select: none;
width: 0;
background: #e1e4e8;
border-left: 1px solid #e1e4e8;
background: $bg-gray;
border-right: 1px solid $border-gray;
overflow: hidden;
&:hover {
&:hover,
&:active {
width: 16px;
background: #fafbfc;
border-right: 1px solid #e1e4e8;
background: $bg-gray-light;
}
.octicon {
@ -207,27 +215,31 @@
font-size: 0;
}
}
.@{name}-side-bar-body {
.#{$name}-side-bar-body {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background: #fafbfc;
border-right: 1px solid #e1e4e8;
border-left: 1px solid #e1e4e8;
background: $bg-gray-light;
border-right: 1px solid $border-gray-light;
border-left: 1px solid $border-gray;
overflow: hidden;
&.hidden {
.hidden;
@extend %hidden;
}
.octicon {
transition: transform 0.3s ease;
color: rgba(3, 47, 98, 0.55);
color: rgba($blue-800, 0.55);
width: 100%;
height: 100%;
}
.octicon-color {
color: rgba($blue-800, 0.55);
}
.octicon-wrapper {
display: inline-block;
width: 16px;
@ -235,11 +247,7 @@
text-align: center;
}
.octicon-color {
color: rgba(3, 47, 98, 0.55);
}
.@{name}-side-bar-content {
.#{$name}-side-bar-content {
display: flex;
flex: 1;
flex-direction: column;
@ -247,20 +255,11 @@
min-height: 0; // make content shrinkable
.meta-bar {
position: relative;
position: relative; // prevent overlap by outline of other elements
padding: 10px;
padding-right: 30px;
font-size: 13px;
line-height: 20px;
color: #586069;
background-color: #f1f8ff;
border-bottom: 1px solid #c8e1ff;
a {
// fix a weird bug:
// when gitako failed loading repo, cursor hovering <a> in meta bar will be 'text'
cursor: pointer;
}
padding-right: 30px; // space for toggle button
background: $bg-blue-light;
border-bottom: 1px solid $border-gray;
.repo-name {
font-weight: bolder;
@ -314,123 +313,111 @@
/* search input */
.search-input-wrapper {
input.form-control {
.search-input {
width: 100%;
&.error {
border-color: #d73a49;
border-color: $red;
}
}
}
.no-results {
padding: 0px 10px;
color: #666;
}
.files {
flex: 1;
overflow: auto;
}
.node-item-row {
background: #fff;
.node-item {
background: $bg-white;
&:hover {
background: #f6f8fa;
background: $bg-gray-light;
}
&.focused {
background: #f0f0f6;
&.focused,
&:active {
background: $bg-gray;
}
&.disabled {
.node-item {
pointer-events: none;
color: #999999;
color: $gray-light;
}
}
display: flex;
justify-content: space-between;
margin: 0;
color: $blue;
line-height: 20px;
padding: 6px 10px;
cursor: pointer;
border-top: 1px solid $border-gray-light;
white-space: nowrap;
@mixin icon-size() {
width: 16px;
height: 16px;
}
.octicon-wrapper:first-child {
margin-right: 6px;
}
&-type-icon {
margin-right: 4px;
@include icon-size();
& + .octicon-wrapper,
& + .node-item-icon {
margin-right: 6px;
}
}
.node-item {
display: flex;
justify-content: space-between;
margin: 0;
color: #0366d6;
line-height: 20px;
padding: 6px 10px;
cursor: pointer;
border-top: 1px solid #eaecef;
transition: all 0.5s ease;
&-icon {
@include icon-size();
object-fit: contain;
vertical-align: text-bottom;
box-sizing: content-box;
&.dim {
filter: sepia(1) hue-rotate(180deg);
}
}
// folder icon rotate when expand
&.expanded .octicon.ChevronRight {
transform: rotate(90deg);
}
.node-item-label {
overflow: hidden;
text-overflow: ellipsis;
}
.go-to-button {
display: none;
white-space: nowrap;
.icon-size() {
width: 16px;
height: 16px;
}
.octicon-wrapper:first-child {
margin-right: 6px;
}
&-type-icon {
margin-right: 4px;
.icon-size();
& + .octicon-wrapper,
& + .node-item-icon {
margin-right: 6px;
}
}
&-icon {
.icon-size();
object-fit: contain;
vertical-align: text-bottom;
box-sizing: content-box;
&.dim {
filter: sepia(1) hue-rotate(180deg);
}
}
// folder icon rotate when expand
&.expanded .octicon.ChevronRight {
transform: rotate(90deg);
}
.node-item-label {
overflow: hidden;
text-overflow: ellipsis;
}
&:hover .node-item-name {
text-decoration: underline;
}
background: transparent;
border: none;
padding: 0;
}
&:hover {
.go-to-button {
display: none;
white-space: nowrap;
background: transparent;
border: none;
padding: 0;
}
&:hover {
.go-to-button {
display: inline-block;
}
display: inline-block;
}
}
}
}
}
.@{name}-settings-bar {
.#{$name}-settings-bar {
z-index: 2;
background: #fafbfc;
background: $bg-gray-light;
display: flex;
flex-direction: column;
max-height: 100%;
&-title {
border-top: 1px solid #eaecef;
padding: 6px 10px;
border-top: 1px solid $border-gray-light;
padding: 10px;
box-shadow: 0 1px 8px 0 rgba(0, 0, 0, 0.2), 0 3px 4px 0 rgba(0, 0, 0, 0.14),
0 3px 3px -2px rgba(0, 0, 0, 0.12);
z-index: 1;
@ -447,58 +434,60 @@
height: 10px;
top: 0;
left: 0;
background: #fafbfc;
background: $bg-gray-light;
z-index: 1;
}
&-section {
.settings-section {
&:not(:last-of-type) {
padding-bottom: 16px;
margin-bottom: 30px;
}
.description:hover {
cursor: help;
.field {
margin-bottom: 4px;
label {
display: inline-block;
margin-bottom: 2px;
font-weight: 600;
}
input {
&:disabled {
cursor: not-allowed;
}
}
select {
width: 100%;
background: $bg-white;
}
&.field-checkbox {
padding-left: 20px;
vertical-align: middle;
label {
margin-bottom: 0;
}
input[type='checkbox'],
input[type='radio'] {
float: left;
margin: 5px 0 0 -20px;
vertical-align: middle;
}
}
}
}
/* inputs for access token & shortcut were too wide */
input.form-control {
min-width: 100px;
}
.form-checkbox {
margin: 0;
}
}
select {
-moz-appearance: none;
}
.form-label {
margin-bottom: 2px;
.link-button {
cursor: pointer;
display: inline-block;
margin-bottom: 4px;
}
.access-token {
border-bottom: none; // prevent overwrite by github style
.link-button {
cursor: pointer;
}
.hint {
color: #6a737d;
}
.hint {
color: $gray;
}
.access-token-input-control {
display: flex;
margin-top: 8px;
.access-token-input {
flex: 1;
&:disabled {
cursor: not-allowed;
}
}
}
.toggle-shortcut {
.hint {
color: #6a737d;
}
}
.toggle-shortcut-input-control {
@ -507,22 +496,14 @@
flex: 1;
}
}
.singleton {
.hint {
color: #6a737d;
}
}
.header-row {
flex-shrink: 0;
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 10px;
border-top: 1px solid #eaecef;
border-top: 1px solid $border-gray-light;
.version {
color: #999999;
}
.show-settings-icon {
width: 20px;
height: 20px;
@ -534,10 +515,6 @@
cursor: pointer;
}
}
input.form-control:not(:last-child) {
margin-right: 5px;
}
}
}
}

View file

@ -3,7 +3,7 @@ import { Gitako } from 'components/Gitako'
import { addMiddleware } from 'driver/connect'
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import './content.less'
import './content.scss'
addMiddleware(withErrorLog)

View file

@ -1,12 +0,0 @@
import * as React from 'react'
export function useDidUpdate(effect: React.EffectCallback, deps?: React.DependencyList) {
const firstTime = React.useRef(true)
React.useEffect(() => {
if (firstTime.current) {
firstTime.current = false
return
}
return effect()
}, deps)
}

View file

@ -1,5 +1,5 @@
import * as React from 'react'
import useLocation from 'react-use/esm/useLocation'
import { useLocation } from 'react-use'
export function useOnLocationChange(
callback: React.EffectCallback,

View file

@ -1,9 +0,0 @@
import * as React from 'react'
export function usePrevious<T>(newValue: T) {
const previousRef = React.useRef(newValue)
React.useEffect(() => {
previousRef.current = newValue
})
return previousRef.current
}

View file

@ -1,12 +0,0 @@
import * as React from 'react'
export function useWindowSize(
callback: (width: number) => void,
deps?: ReadonlyArray<any> | undefined,
) {
React.useEffect(() => {
const resizeListener: (this: Window, ev: UIEvent) => void = () => callback(window.innerWidth)
window.addEventListener('resize', resizeListener)
return () => window.removeEventListener('resize', resizeListener)
}, deps)
}

View file

@ -1,15 +1,13 @@
{
"compilerOptions": {
"target": "es2016",
"outDir": "dist",
"moduleResolution": "Node",
"module": "ESNext",
"target": "ESNext",
"jsx": "react",
"allowJs": true,
"module": "commonjs",
"strict": true,
"resolveJsonModule": true,
"lib": ["dom", "es2017.object", "es2016"],
"baseUrl": ".",
"experimentalDecorators": true,
"outDir": "dist",
"paths": {
"*": ["src/*", "packages/*"]
}

View file

@ -81,10 +81,12 @@ module.exports = {
cacheDirectory: true,
},
include: [srcPath, packagesPath],
exclude: /node_modules/,
sideEffects: false,
},
{
test: /\.less$/,
loader: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader'],
test: /\.scss$/,
loader: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
include: [srcPath],
},
{

1123
yarn.lock

File diff suppressed because it is too large Load diff