Project conflict indicators in outline panel

* OutlinePanel has a badge showing the number of conflicting projects
* Each project list item gets a highlight if it has conflicts
* Refactored Popover to prepare to merge it with tooltips
This commit is contained in:
Aria Minaei 2021-08-12 11:52:23 +02:00
parent e50daa3ba7
commit 72d40bf204
8 changed files with 57 additions and 39 deletions

View file

@ -134,10 +134,11 @@ const SnapshotEditor: React.FC<{paneId: string}> = (props) => {
<Overlay>
<Tools>
<ToolbarIconButton
icon={<IoCameraReverseOutline />}
label="Refresh Snapshot"
title="Refresh Snapshot"
onClick={createSnapshot}
></ToolbarIconButton>
>
<IoCameraReverseOutline />
</ToolbarIconButton>
</Tools>
</Overlay>

View file

@ -26,9 +26,10 @@ const Toolbar: VFC = () => {
onClick={() => {
studio.createPane('snapshotEditor')
}}
icon={<IoCameraOutline />}
label="Create snapshot"
/>
title="Create snapshot"
>
<IoCameraOutline />
</ToolbarIconButton>
<TransformControlsModeSelect
value={transformControlsMode}
onChange={(value) =>

View file

@ -5,6 +5,9 @@ import ProjectsList from './ProjectsList/ProjectsList'
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
import ToolbarIconButton from '@theatre/studio/uiComponents/toolbar/ToolbarIconButton'
import {VscListTree} from 'react-icons/all'
import {usePrism} from '@theatre/dataverse-react'
import getStudio from '@theatre/studio/getStudio'
import {val} from '@theatre/dataverse'
const Container = styled.div`
background-color: transparent;
@ -103,11 +106,45 @@ const Body = styled.div`
user-select: none;
`
const NumberOfConflictsIndicator = styled.div`
color: white;
width: 14px;
height: 14px;
background: #d00;
border-radius: 4px;
text-align: center;
line-height: 14px;
font-weight: 600;
font-size: 8px;
position: relative;
left: -6px;
top: -11px;
margin-right: -14px;
box-shadow: 0 4px 6px -4px #00000059;
`
const OutlinePanel: React.FC<{}> = (props) => {
const conflicts = usePrism(() => {
const ephemeralStateOfAllProjects = val(
getStudio().atomP.ephemeral.coreByProject,
)
return Object.entries(ephemeralStateOfAllProjects).filter(
([a, state]) =>
state.loadingState.type === 'browserStateIsNotBasedOnDiskState',
)
}, [])
return (
<Container>
<TriggerContainer>
<TriggerButton icon={<VscListTree />} label="Outline" />
<TriggerButton title="Outline">
<VscListTree />
</TriggerButton>
{conflicts.length > 0 ? (
<NumberOfConflictsIndicator>
{conflicts.length}
</NumberOfConflictsIndicator>
) : null}
<Title>Outline</Title>
</TriggerContainer>
<Content>

View file

@ -65,6 +65,7 @@ const HitZone = styled.div`
display: block;
content: ' ';
background: url(${SnapCursor}) no-repeat 100% 100%;
// This icon might also fit: GiConvergenceTarget
}
}

View file

@ -29,11 +29,12 @@ const Container = styled.ul`
`
const Popover: React.FC<{
clickPoint: {clientX: number; clientY: number}
clickPoint?: {clientX: number; clientY: number}
target: HTMLElement
onRequestClose: () => void
onPointerOutOfThreshold: () => void
children: () => React.ReactNode
pointerDistanceThreshold?: number
className?: string
}> = (props) => {
const pointerDistanceThreshold =
props.pointerDistanceThreshold ?? defaultPointerDistanceThreshold
@ -101,13 +102,13 @@ const Popover: React.FC<{
e.clientY < pos.top - pointerDistanceThreshold ||
e.clientY > pos.top + containerRect.height + pointerDistanceThreshold
) {
props.onRequestClose()
props.onPointerOutOfThreshold()
}
}
const onMouseDown = (e: MouseEvent) => {
if (!e.composedPath().includes(container)) {
props.onRequestClose()
props.onPointerOutOfThreshold()
}
}
@ -125,11 +126,11 @@ const Popover: React.FC<{
props.target,
targetRect,
windowSize,
props.onRequestClose,
props.onPointerOutOfThreshold,
])
return createPortal(
<Container ref={setContainer}>
<Container ref={setContainer} className={props.className}>
<PopoverArrow ref={arrowRef} />
{props.children()}
</Container>,

View file

@ -38,7 +38,7 @@ export default function usePopover(
children={render}
clickPoint={state.clickPoint}
target={state.target}
onRequestClose={close}
onPointerOutOfThreshold={close}
/>
) : (
<></>

View file

@ -1,14 +1,11 @@
import type {ReactElement} from 'react'
import React from 'react'
import styled from 'styled-components'
import type {ButtonProps} from 'reakit'
import {outlinePanelTheme} from '@theatre/studio/panels/OutlinePanel/BaseItem'
import {darken, opacify} from 'polished'
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
const {baseBg, baseBorderColor, baseFontColor} = outlinePanelTheme
export const TheButton = styled.button`
const ToolbarIconButton = styled.button`
${pointerEventsAutoInNormalMode};
position: relative;
display: flex;
@ -55,24 +52,4 @@ export const TheButton = styled.button`
border: 0;
`
const ToolbarIconButton: React.FC<
Exclude<ButtonProps, 'children'> & {
icon: ReactElement
label: string
}
> = ({label, icon, ...props}) => {
return (
<>
<TheButton
aria-label={label}
onClick={props.onClick}
title={label}
className={props.className}
>
{icon}
</TheButton>
</>
)
}
export default ToolbarIconButton

View file

@ -3,7 +3,7 @@ import React from 'react'
import type {IconType} from 'react-icons'
import {Group, Button} from 'reakit'
import styled from 'styled-components'
import {TheButton as ButtonImpl} from './ToolbarIconButton'
import ButtonImpl from './ToolbarIconButton'
const Opt = styled(ButtonImpl)``