feat: sidebar on the right side

This commit is contained in:
EnixCoda 2024-07-15 12:51:48 +08:00
parent 897a116384
commit 39a2383b8c
11 changed files with 190 additions and 47 deletions

View file

@ -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

View file

@ -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]),
)
}

View file

@ -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}
/>
)

View file

@ -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,

View file

@ -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',

View file

@ -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));
}
}
}
}
}

View file

@ -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));
}
}
}
}
}

View file

@ -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 {

View file

@ -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}`)
}
/**

View file

@ -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)

View file

@ -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) => {