mirror of
https://github.com/EnixCoda/Gitako.git
synced 2026-03-11 08:54:44 +00:00
feat: sidebar on the right side
This commit is contained in:
parent
897a116384
commit
39a2383b8c
11 changed files with 190 additions and 47 deletions
|
|
@ -1,19 +1,19 @@
|
|||
import { GrabberIcon } from '@primer/octicons-react'
|
||||
import { Icon } from 'components/Icon'
|
||||
import * as React from 'react'
|
||||
import { ResizeState, useResizeHandler } from '../utils/hooks/useResizeHandler'
|
||||
import { ResizeHandlerOptions, useResizeHandler } from '../utils/hooks/useResizeHandler'
|
||||
import { Size2D } from './Size'
|
||||
|
||||
type Props = {
|
||||
size: Size2D
|
||||
onResize(size: Size2D): void
|
||||
onResetSize?(): void
|
||||
onResizeStateChange?(state: ResizeState): void
|
||||
options?: ResizeHandlerOptions
|
||||
style?: React.CSSProperties
|
||||
}
|
||||
|
||||
export function ResizeHandler({ onResize, onResetSize, onResizeStateChange, size, style }: Props) {
|
||||
const { onPointerDown } = useResizeHandler(size, onResize, { onResizeStateChange })
|
||||
export function ResizeHandler({ onResize, onResetSize, options, size, style }: Props) {
|
||||
const { onPointerDown } = useResizeHandler(size, onResize, options)
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -63,6 +63,8 @@ export function SideBar() {
|
|||
|
||||
const sidebarContextValue = React.useMemo(() => ({ pendingFocusTarget }), [pendingFocusTarget])
|
||||
|
||||
const placement = configContext.value.sidebarPlacement
|
||||
|
||||
return (
|
||||
<Theme>
|
||||
<ToggleShowButtonWrapper
|
||||
|
|
@ -73,15 +75,23 @@ export function SideBar() {
|
|||
<SidebarContext.Provider value={sidebarContextValue}>
|
||||
<div className={'gitako-side-bar'}>
|
||||
<div
|
||||
className={cx('gitako-side-bar-body-wrapper', `toggle-mode-${sidebarToggleMode}`, {
|
||||
collapsed: error || !shouldExpand,
|
||||
})}
|
||||
className={cx(
|
||||
'gitako-side-bar-body-wrapper',
|
||||
`toggle-mode-${sidebarToggleMode}`,
|
||||
`placement-${placement}`,
|
||||
{
|
||||
collapsed: error || !shouldExpand,
|
||||
},
|
||||
)}
|
||||
style={{ height: heightForSafari }}
|
||||
onMouseLeave={() => {
|
||||
if (blockLeaveRef.current) return
|
||||
if (sidebarToggleMode === 'float') setShouldExpand(false)
|
||||
}}
|
||||
>
|
||||
{features.resize && placement === 'right' && (
|
||||
<SideBarResizeHandler onResizeStateChange={onResizeStateChange} />
|
||||
)}
|
||||
<div className={'gitako-side-bar-body'}>
|
||||
<div className={'gitako-side-bar-content'}>
|
||||
<div className={'header'}>
|
||||
|
|
@ -156,7 +166,9 @@ export function SideBar() {
|
|||
}}
|
||||
</IIFC>
|
||||
</div>
|
||||
{features.resize && <SideBarResizeHandler onResizeStateChange={onResizeStateChange} />}
|
||||
{features.resize && placement === 'left' && (
|
||||
<SideBarResizeHandler onResizeStateChange={onResizeStateChange} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</SidebarContext.Provider>
|
||||
|
|
@ -220,17 +232,17 @@ function useLogoContainerElement() {
|
|||
}
|
||||
|
||||
function useUpdateBodyIndentOnStateUpdate(shouldExpand: boolean) {
|
||||
const { sidebarToggleMode } = useConfigs().value
|
||||
const { sidebarToggleMode, sidebarPlacement } = useConfigs().value
|
||||
React.useEffect(() => {
|
||||
if (!(sidebarToggleMode === 'persistent' && shouldExpand)) return
|
||||
|
||||
const detach = DOMHelper.attachStickyBodyIndent()
|
||||
DOMHelper.setBodyIndent(true)
|
||||
DOMHelper.setBodyIndent(sidebarPlacement)
|
||||
return () => {
|
||||
detach()
|
||||
DOMHelper.setBodyIndent(false)
|
||||
}
|
||||
}, [sidebarToggleMode, shouldExpand])
|
||||
}, [sidebarToggleMode, shouldExpand, sidebarPlacement])
|
||||
}
|
||||
|
||||
const getDerivedExpansion = ({
|
||||
|
|
@ -252,7 +264,7 @@ function useGetDerivedExpansion() {
|
|||
}
|
||||
|
||||
function useUpdateBodyIndentAfterRedirect(update: (shouldExpand: boolean) => void) {
|
||||
const { intelligentToggle, sidebarToggleMode } = useConfigs().value
|
||||
const { intelligentToggle, sidebarToggleMode, sidebarPlacement } = useConfigs().value
|
||||
useAfterRedirect(
|
||||
React.useCallback(() => {
|
||||
// check and update expand state if pinned and auto-expand checked
|
||||
|
|
@ -260,9 +272,9 @@ function useUpdateBodyIndentAfterRedirect(update: (shouldExpand: boolean) => voi
|
|||
const shouldExpand = getDerivedExpansion({ intelligentToggle, sidebarToggleMode })
|
||||
update(shouldExpand)
|
||||
// Below DOM mutation cannot be omitted, if do, body indent may get lost when shouldExpand is true for both before & after redirecting
|
||||
DOMHelper.setBodyIndent(shouldExpand)
|
||||
DOMHelper.setBodyIndent(sidebarPlacement)
|
||||
}
|
||||
}, [update, sidebarToggleMode, intelligentToggle]),
|
||||
}, [update, sidebarToggleMode, intelligentToggle, sidebarPlacement]),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useDebounce, useLatest, useWindowSize } from 'react-use'
|
|||
import { getDefaultConfigs } from 'utils/config/helper'
|
||||
import * as DOMHelper from 'utils/DOMHelper'
|
||||
import { useAfterRedirect } from 'utils/hooks/useFastRedirect'
|
||||
import { ResizeHandlerOptions } from 'utils/hooks/useResizeHandler'
|
||||
import { getSafeWidth } from '../utils/getSafeWidth'
|
||||
import { ResizeHandler } from './ResizeHandler'
|
||||
import { Size, Size2D } from './Size'
|
||||
|
|
@ -41,7 +42,7 @@ function useSidebarWidth() {
|
|||
|
||||
export function SideBarResizeHandler({
|
||||
onResizeStateChange,
|
||||
}: Pick<React.ComponentProps<typeof ResizeHandler>, 'onResizeStateChange'>) {
|
||||
}: Pick<ResizeHandlerOptions, 'onResizeStateChange'>) {
|
||||
const [width, setWidth] = useSidebarWidth()
|
||||
const { width: windowWidth } = useWindowSize()
|
||||
const onResize = React.useMemo(() => {
|
||||
|
|
@ -71,11 +72,16 @@ export function SideBarResizeHandler({
|
|||
|
||||
const dummySize: Size2D = React.useMemo(() => [width, 0], [width])
|
||||
|
||||
const placement = useConfigs().value.sidebarPlacement
|
||||
|
||||
return (
|
||||
<ResizeHandler
|
||||
onResize={onResize}
|
||||
onResetSize={onResetSize}
|
||||
onResizeStateChange={onResizeStateChange}
|
||||
options={{
|
||||
onResizeStateChange,
|
||||
direction: placement === 'left' ? 'right' : 'left',
|
||||
}}
|
||||
size={dummySize}
|
||||
/>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -63,8 +63,13 @@ export function ToggleShowButton({ className, onClick, onHover }: Props) {
|
|||
{ onClick },
|
||||
)
|
||||
|
||||
const placement = config.value.sidebarPlacement
|
||||
|
||||
return (
|
||||
<div ref={ref} className={cx('gitako-toggle-show-button-wrapper', className)}>
|
||||
<div
|
||||
ref={ref}
|
||||
className={cx('gitako-toggle-show-button-wrapper', `placement-${placement}`, className)}
|
||||
>
|
||||
<button
|
||||
className={cx('gitako-toggle-show-button', {
|
||||
error,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import * as React from 'react'
|
|||
import { subIO } from 'utils/general'
|
||||
import { KeyboardShortcutSetting } from './KeyboardShortcutSetting'
|
||||
import { SettingsSection } from './SettingsSection'
|
||||
import { SimpleConfigFieldSelect } from './SimpleConfigField/SelectInput'
|
||||
|
||||
export function SidebarSettings() {
|
||||
const { sidebarToggleMode } = useConfigs().value
|
||||
|
|
@ -18,6 +19,25 @@ export function SidebarSettings() {
|
|||
label={'Keyboard shortcut for focus search input'}
|
||||
{...subIO(useConfigs(), 'focusSearchInputShortcut')}
|
||||
/>
|
||||
<SimpleConfigFieldSelect
|
||||
field={{
|
||||
key: 'sidebarPlacement',
|
||||
label: 'Sidebar placement',
|
||||
tooltip: 'Change the position of the sidebar',
|
||||
}}
|
||||
options={[
|
||||
{
|
||||
key: 'left',
|
||||
value: 'left',
|
||||
label: 'Left',
|
||||
},
|
||||
{
|
||||
key: 'right',
|
||||
value: 'right',
|
||||
label: 'Right',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<SimpleConfigFieldCheckbox
|
||||
field={{
|
||||
key: 'intelligentToggle',
|
||||
|
|
|
|||
|
|
@ -29,13 +29,19 @@ $gitee-header-z-index: 1002;
|
|||
}
|
||||
}
|
||||
|
||||
&[data-with-gitako-spacing='true'] {
|
||||
&[data-with-gitako-spacing='left'] {
|
||||
body {
|
||||
width: auto; // shrink width
|
||||
.site-content {
|
||||
min-width: 1040px;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-with-gitako-spacing='right'] {
|
||||
body {
|
||||
min-width: calc(1040px + var(--gitako-width));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
@mixin github($github-content-width) {
|
||||
[data-gitako-platform='GitHub'] {
|
||||
@media (min-width: $github-content-width) {
|
||||
@media (min-width: $github-content-width) {
|
||||
[data-gitako-platform='GitHub'] {
|
||||
body {
|
||||
min-width: $github-content-width;
|
||||
}
|
||||
|
||||
&[data-with-gitako-spacing='right'] {
|
||||
body {
|
||||
min-width: calc(#{$github-content-width} + var(--gitako-width));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,12 +40,23 @@ $minimal-z-index: max(
|
|||
}
|
||||
}
|
||||
|
||||
html[data-with-gitako-spacing='true'] {
|
||||
body,
|
||||
html {
|
||||
&[data-with-gitako-spacing='left'] {
|
||||
body,
|
||||
.AppHeader deferred-side-panel div[data-modal-dialog-overlay] // side panel opened via clicking github top-left icon in global navigation mode
|
||||
{
|
||||
@media screen {
|
||||
margin-left: var(--gitako-width);
|
||||
{
|
||||
@media screen {
|
||||
margin-left: var(--gitako-width);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-with-gitako-spacing='right'] {
|
||||
body,
|
||||
.AppHeader deferred-side-panel div[data-modal-dialog-overlay] {
|
||||
@media screen {
|
||||
margin-right: var(--gitako-width);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -72,13 +83,55 @@ html[data-with-gitako-spacing='true'] {
|
|||
z-index: $minimal-z-index;
|
||||
position: fixed;
|
||||
top: 124px; // align with GitHub's navbar items
|
||||
left: 0px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
transition: left 0.25s linear;
|
||||
transition: all 0.25s linear;
|
||||
transition-property: left, right;
|
||||
|
||||
&.hidden {
|
||||
left: -40px;
|
||||
&.placement-left {
|
||||
left: 0px;
|
||||
|
||||
&.hidden {
|
||||
left: -40px;
|
||||
}
|
||||
|
||||
.#{$name}-toggle-show-button {
|
||||
left: -8px;
|
||||
|
||||
.tentacle {
|
||||
transform: translateX(-8px);
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:hover {
|
||||
.tentacle {
|
||||
transform: translateX(-4px);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.placement-right {
|
||||
right: 0;
|
||||
|
||||
&.hidden {
|
||||
right: -40px;
|
||||
}
|
||||
|
||||
.#{$name}-toggle-show-button {
|
||||
left: 8px;
|
||||
|
||||
.tentacle {
|
||||
transform: translateX(8px) scale(-1, 1);
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:hover {
|
||||
.tentacle {
|
||||
transform: translateX(4px) scale(-1, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.#{$name}-toggle-show-button {
|
||||
|
|
@ -87,7 +140,6 @@ html[data-with-gitako-spacing='true'] {
|
|||
border: none;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
left: -8px;
|
||||
|
||||
&.error {
|
||||
cursor: not-allowed;
|
||||
|
|
@ -98,13 +150,11 @@ html[data-with-gitako-spacing='true'] {
|
|||
height: 40px;
|
||||
object-fit: contain;
|
||||
transition: all ease 0.4s;
|
||||
transform: translateX(-8px);
|
||||
filter: drop-shadow(0 0 1px var(--color-primer-canvas-backdrop));
|
||||
}
|
||||
&:active,
|
||||
&:hover {
|
||||
.tentacle {
|
||||
transform: translateX(-4px);
|
||||
filter: drop-shadow(0 0 2px var(--color-primer-canvas-backdrop));
|
||||
}
|
||||
}
|
||||
|
|
@ -136,11 +186,32 @@ html[data-with-gitako-spacing='true'] {
|
|||
.#{$name}-side-bar-body-wrapper {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100vh;
|
||||
z-index: $minimal-z-index;
|
||||
display: flex;
|
||||
|
||||
&.placement-left {
|
||||
left: 0;
|
||||
|
||||
&.toggle-mode-float {
|
||||
left: 0;
|
||||
&.collapsed {
|
||||
left: calc(0px - #{$resizeHandlerWidth} - var(--gitako-width));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.placement-right {
|
||||
right: 0;
|
||||
|
||||
&.toggle-mode-float {
|
||||
right: 0;
|
||||
&.collapsed {
|
||||
right: calc(0px - #{$resizeHandlerWidth} - var(--gitako-width));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.toggle-mode-persistent {
|
||||
&.collapsed {
|
||||
@extend %hidden;
|
||||
|
|
@ -148,11 +219,7 @@ html[data-with-gitako-spacing='true'] {
|
|||
}
|
||||
|
||||
&.toggle-mode-float {
|
||||
left: 0;
|
||||
transition: all 0.25s cubic-bezier(0.55, 0.06, 0.68, 0.19); // values from Chrome devtools
|
||||
&.collapsed {
|
||||
left: calc(0px - #{$resizeHandlerWidth} - var(--gitako-width));
|
||||
}
|
||||
}
|
||||
}
|
||||
.#{$name}-resize-handler {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
import { platformName } from 'platforms'
|
||||
import { $ } from './$'
|
||||
import { Config } from './config/helper'
|
||||
|
||||
export const rootElementID = 'gitako-root'
|
||||
export const gitakoDescriptionTarget = document.documentElement
|
||||
|
|
@ -82,10 +83,19 @@ export function markGitakoPlatform() {
|
|||
const spacingAttributeName = 'data-with-gitako-spacing'
|
||||
export const attachStickyBodyIndent = () =>
|
||||
attachStickyDataAttribute(gitakoDescriptionTarget, spacingAttributeName, ({ oldValue }) =>
|
||||
setBodyIndent(oldValue === 'true'),
|
||||
setBodyIndent(
|
||||
(oldValue &&
|
||||
(
|
||||
{
|
||||
left: 'left',
|
||||
right: 'right',
|
||||
} as const
|
||||
)[oldValue]) ||
|
||||
'left',
|
||||
),
|
||||
)
|
||||
export function setBodyIndent(shouldShowGitako: boolean) {
|
||||
gitakoDescriptionTarget.setAttribute(spacingAttributeName, `${shouldShowGitako}`)
|
||||
export function setBodyIndent(placement: Config['sidebarPlacement'] | false) {
|
||||
gitakoDescriptionTarget.setAttribute(spacingAttributeName, `${placement}`)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ export type Config = {
|
|||
restoreExpandedFolders: boolean
|
||||
pjaxMode: 'native' | 'pjax-api'
|
||||
showDiffInText: boolean
|
||||
sidebarPlacement: 'left' | 'right'
|
||||
__showInspector?: boolean
|
||||
}
|
||||
|
||||
|
|
@ -46,6 +47,7 @@ enum configKeys {
|
|||
restoreExpandedFolders = 'restoreExpandedFolders',
|
||||
pjaxMode = 'pjaxMode',
|
||||
showDiffInText = 'showDiffInText',
|
||||
sidebarPlacement = 'sidebarPlacement',
|
||||
__showInspector = '__showInspector',
|
||||
}
|
||||
|
||||
|
|
@ -72,6 +74,7 @@ export const getDefaultConfigs: () => Config = () => ({
|
|||
restoreExpandedFolders: true,
|
||||
pjaxMode: platformName === 'GitHub' ? 'native' : 'pjax-api', // use native on GitHub
|
||||
showDiffInText: false,
|
||||
sidebarPlacement: 'left',
|
||||
})
|
||||
|
||||
const configKeyArray = Object.values(configKeys)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,13 @@ import { Size2D } from '../../components/Size'
|
|||
|
||||
export type ResizeState = 'idle' | 'resizing'
|
||||
|
||||
export type ResizeHandlerOptions = Partial<{
|
||||
onResizeStateChange: (state: ResizeState) => void
|
||||
onClick: (e: PointerEvent) => void
|
||||
distanceTolerance: number
|
||||
direction: 'right' | 'left'
|
||||
}>
|
||||
|
||||
export function useResizeHandler(
|
||||
size: Size2D,
|
||||
onResize: (size: Size2D) => void,
|
||||
|
|
@ -10,17 +17,18 @@ export function useResizeHandler(
|
|||
onResizeStateChange,
|
||||
onClick,
|
||||
distanceTolerance = 2,
|
||||
}: Partial<{
|
||||
onResizeStateChange: (state: ResizeState) => void
|
||||
onClick: (e: PointerEvent) => void
|
||||
distanceTolerance: number
|
||||
}> = {},
|
||||
direction = 'right',
|
||||
}: ResizeHandlerOptions = {},
|
||||
) {
|
||||
const pointerDown = React.useRef(false)
|
||||
const pointerMoved = React.useRef(false)
|
||||
const initialSizeRef = React.useRef([0, 0])
|
||||
const baseSize = React.useRef(size)
|
||||
const latestPropSize = React.useRef(size)
|
||||
const fix = {
|
||||
left: -1,
|
||||
right: 1,
|
||||
}[direction]
|
||||
|
||||
React.useEffect(() => {
|
||||
latestPropSize.current = size
|
||||
|
|
@ -34,11 +42,11 @@ export function useResizeHandler(
|
|||
pointerMoved.current =
|
||||
pointerMoved.current || (clientX - x0) ** 2 + (clientY - y0) ** 2 > distanceTolerance ** 2
|
||||
const [x1, y1] = baseSize.current
|
||||
onResize([x1 + clientX - x0, y1 + clientY - y0])
|
||||
onResize([x1 + (clientX - x0) * fix, y1 + (clientY - y0) * fix])
|
||||
}
|
||||
window.addEventListener('pointermove', onPointerMove)
|
||||
return () => window.removeEventListener('pointermove', onPointerMove)
|
||||
}, [onResize, distanceTolerance])
|
||||
}, [onResize, distanceTolerance, fix])
|
||||
|
||||
React.useEffect(() => {
|
||||
const onPointerUp = (e: PointerEvent) => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue