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> <Overlay>
<Tools> <Tools>
<ToolbarIconButton <ToolbarIconButton
icon={<IoCameraReverseOutline />} title="Refresh Snapshot"
label="Refresh Snapshot"
onClick={createSnapshot} onClick={createSnapshot}
></ToolbarIconButton> >
<IoCameraReverseOutline />
</ToolbarIconButton>
</Tools> </Tools>
</Overlay> </Overlay>

View file

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

View file

@ -5,6 +5,9 @@ import ProjectsList from './ProjectsList/ProjectsList'
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css' import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
import ToolbarIconButton from '@theatre/studio/uiComponents/toolbar/ToolbarIconButton' import ToolbarIconButton from '@theatre/studio/uiComponents/toolbar/ToolbarIconButton'
import {VscListTree} from 'react-icons/all' 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` const Container = styled.div`
background-color: transparent; background-color: transparent;
@ -103,11 +106,45 @@ const Body = styled.div`
user-select: none; 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 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 ( return (
<Container> <Container>
<TriggerContainer> <TriggerContainer>
<TriggerButton icon={<VscListTree />} label="Outline" /> <TriggerButton title="Outline">
<VscListTree />
</TriggerButton>
{conflicts.length > 0 ? (
<NumberOfConflictsIndicator>
{conflicts.length}
</NumberOfConflictsIndicator>
) : null}
<Title>Outline</Title> <Title>Outline</Title>
</TriggerContainer> </TriggerContainer>
<Content> <Content>

View file

@ -65,6 +65,7 @@ const HitZone = styled.div`
display: block; display: block;
content: ' '; content: ' ';
background: url(${SnapCursor}) no-repeat 100% 100%; 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<{ const Popover: React.FC<{
clickPoint: {clientX: number; clientY: number} clickPoint?: {clientX: number; clientY: number}
target: HTMLElement target: HTMLElement
onRequestClose: () => void onPointerOutOfThreshold: () => void
children: () => React.ReactNode children: () => React.ReactNode
pointerDistanceThreshold?: number pointerDistanceThreshold?: number
className?: string
}> = (props) => { }> = (props) => {
const pointerDistanceThreshold = const pointerDistanceThreshold =
props.pointerDistanceThreshold ?? defaultPointerDistanceThreshold props.pointerDistanceThreshold ?? defaultPointerDistanceThreshold
@ -101,13 +102,13 @@ const Popover: React.FC<{
e.clientY < pos.top - pointerDistanceThreshold || e.clientY < pos.top - pointerDistanceThreshold ||
e.clientY > pos.top + containerRect.height + pointerDistanceThreshold e.clientY > pos.top + containerRect.height + pointerDistanceThreshold
) { ) {
props.onRequestClose() props.onPointerOutOfThreshold()
} }
} }
const onMouseDown = (e: MouseEvent) => { const onMouseDown = (e: MouseEvent) => {
if (!e.composedPath().includes(container)) { if (!e.composedPath().includes(container)) {
props.onRequestClose() props.onPointerOutOfThreshold()
} }
} }
@ -125,11 +126,11 @@ const Popover: React.FC<{
props.target, props.target,
targetRect, targetRect,
windowSize, windowSize,
props.onRequestClose, props.onPointerOutOfThreshold,
]) ])
return createPortal( return createPortal(
<Container ref={setContainer}> <Container ref={setContainer} className={props.className}>
<PopoverArrow ref={arrowRef} /> <PopoverArrow ref={arrowRef} />
{props.children()} {props.children()}
</Container>, </Container>,

View file

@ -38,7 +38,7 @@ export default function usePopover(
children={render} children={render}
clickPoint={state.clickPoint} clickPoint={state.clickPoint}
target={state.target} 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 styled from 'styled-components'
import type {ButtonProps} from 'reakit'
import {outlinePanelTheme} from '@theatre/studio/panels/OutlinePanel/BaseItem' import {outlinePanelTheme} from '@theatre/studio/panels/OutlinePanel/BaseItem'
import {darken, opacify} from 'polished' import {darken, opacify} from 'polished'
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css' import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
const {baseBg, baseBorderColor, baseFontColor} = outlinePanelTheme const {baseBg, baseBorderColor, baseFontColor} = outlinePanelTheme
export const TheButton = styled.button` const ToolbarIconButton = styled.button`
${pointerEventsAutoInNormalMode}; ${pointerEventsAutoInNormalMode};
position: relative; position: relative;
display: flex; display: flex;
@ -55,24 +52,4 @@ export const TheButton = styled.button`
border: 0; 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 export default ToolbarIconButton

View file

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