UI improvements/sidebar pinning (#175)

Co-authored-by: Aria Minaei <aria.minaei@gmail.com>
This commit is contained in:
Andrew Prifer 2022-05-25 20:42:01 +02:00 committed by GitHub
parent ec18687a98
commit 0690a85ae2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 998 additions and 501 deletions

View file

@ -12,7 +12,12 @@ test.describe('setting-static-props', () => {
test('Undo/redo', async ({page}) => { test('Undo/redo', async ({page}) => {
await page.locator('[data-testid="OutlinePanel-TriggerButton"]').click() await page.locator('[data-testid="OutlinePanel-TriggerButton"]').click()
await page.locator('span:has-text("sample object")').first().click() // https://github.com/microsoft/playwright/issues/12298
// The div does in fact intercept pointer events, but it is meant to 🤦‍
await page
.locator('span:has-text("sample object")')
.first()
.click({force: true})
const detailPanel = page.locator('[data-testid="DetailPanel-Object"]') const detailPanel = page.locator('[data-testid="DetailPanel-Object"]')

View file

@ -99,8 +99,6 @@ const ReferenceWindow: VFC<ReferenceWindowProps> = ({height}) => {
const ctx = canvasRef.current!.getContext('2d')! const ctx = canvasRef.current!.getContext('2d')!
// console.log(gl.domElement.getContext('webgl2')!.getContextAttributes())
// https://stackoverflow.com/questions/17861447/html5-canvas-drawimage-how-to-apply-antialiasing // https://stackoverflow.com/questions/17861447/html5-canvas-drawimage-how-to-apply-antialiasing
ctx.imageSmoothingQuality = 'high' ctx.imageSmoothingQuality = 'high'

View file

@ -15,7 +15,6 @@ import {getEditorSheet, getEditorSheetObject} from './editorStuff'
import type {$IntentionalAny} from '@theatre/shared/utils/types' import type {$IntentionalAny} from '@theatre/shared/utils/types'
import {InfiniteGridHelper} from '../InfiniteGridHelper' import {InfiniteGridHelper} from '../InfiniteGridHelper'
import {DragDetectorProvider} from './DragDetector' import {DragDetectorProvider} from './DragDetector'
import TooltipPortalProvider from './TooltipPortalProvider'
import ReferenceWindow from './ReferenceWindow/ReferenceWindow' import ReferenceWindow from './ReferenceWindow/ReferenceWindow'
const GlobalStyle = createGlobalStyle` const GlobalStyle = createGlobalStyle`
@ -182,39 +181,37 @@ const SnapshotEditor: React.FC<{paneId: string}> = (props) => {
<StyleSheetManager disableVendorPrefixes> <StyleSheetManager disableVendorPrefixes>
<> <>
<GlobalStyle /> <GlobalStyle />
<TooltipPortalProvider> <Wrapper>
<Wrapper> <Overlay>
<Overlay> <Tools ref={setToolsContainer} />
<Tools ref={setToolsContainer} /> {showReferenceWindow && (
{showReferenceWindow && ( <ReferenceWindowContainer>
<ReferenceWindowContainer> <ReferenceWindow height={120} />
<ReferenceWindow height={120} /> </ReferenceWindowContainer>
</ReferenceWindowContainer> )}
)} </Overlay>
</Overlay>
{sceneSnapshot ? ( {sceneSnapshot ? (
<> <>
<CanvasWrapper> <CanvasWrapper>
<Canvas <Canvas
onCreated={({gl}) => { onCreated={({gl}) => {
gl.setClearColor('white') gl.setClearColor('white')
}} }}
shadows shadows
dpr={[1, 2]} dpr={[1, 2]}
frameloop="demand" frameloop="demand"
onPointerMissed={onPointerMissed} onPointerMissed={onPointerMissed}
> >
<EditorScene <EditorScene
snapshotEditorSheet={snapshotEditorSheet} snapshotEditorSheet={snapshotEditorSheet}
paneId={paneId} paneId={paneId}
/> />
</Canvas> </Canvas>
</CanvasWrapper> </CanvasWrapper>
</> </>
) : null} ) : null}
</Wrapper> </Wrapper>
</TooltipPortalProvider>
</> </>
</StyleSheetManager> </StyleSheetManager>
</root.div> </root.div>

View file

@ -1,27 +0,0 @@
import type {ReactNode} from 'react'
import React, {useState} from 'react'
import {PortalContext} from 'reakit'
import styled from 'styled-components'
const PortalHost = styled.div`
position: fixed;
inset: 0;
pointer-events: none;
`
export interface PortalManagerProps {
children: ReactNode
}
const TooltipPortalProvider: React.VFC<PortalManagerProps> = ({children}) => {
const [wrapper, setWrapper] = useState<HTMLDivElement | null>(null)
return (
<PortalContext.Provider value={wrapper}>
{children}
<PortalHost ref={setWrapper} />
</PortalContext.Provider>
)
}
export default TooltipPortalProvider

View file

@ -21,6 +21,27 @@ const r3fExtension: IExtension = {
id: '@theatre/r3f', id: '@theatre/r3f',
toolbars: { toolbars: {
global(set, studio) { global(set, studio) {
const calc = prism<ToolsetConfig>(() => {
const editorObject = getEditorSheetObject()
return [
{
type: 'Icon',
title: 'Create Snapshot',
svgSource: io5CameraOutline,
onClick: () => {
studio.createPane('snapshot')
},
},
]
})
return calc.tapImmediate(Ticker.raf, () => {
set(calc.getValue())
})
},
'snapshot-editor': (set, studio) => {
const {createSnapshot} = useEditorStore.getState()
const calc = prism<ToolsetConfig>(() => { const calc = prism<ToolsetConfig>(() => {
const editorObject = getEditorSheetObject() const editorObject = getEditorSheetObject()
@ -34,11 +55,9 @@ const r3fExtension: IExtension = {
return [ return [
{ {
type: 'Icon', type: 'Icon',
title: 'Create Snapshot', onClick: createSnapshot,
svgSource: io5CameraOutline, title: 'Refresh Snapshot',
onClick: () => { svgSource: `<svg stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>`,
studio.createPane('snapshot')
},
}, },
{ {
type: 'Switch', type: 'Switch',
@ -121,19 +140,6 @@ const r3fExtension: IExtension = {
set(calc.getValue()) set(calc.getValue())
}) })
}, },
'snapshot-editor': (set) => {
const {createSnapshot} = useEditorStore.getState()
const onClick = createSnapshot
set([
{
type: 'Icon',
onClick,
title: 'Refresh Snapshot',
svgSource: `<svg stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>`,
},
])
return () => {}
},
}, },
panes: [ panes: [
{ {

View file

@ -5,8 +5,8 @@ import ReactDOM from 'react-dom'
import type {Studio} from './Studio' import type {Studio} from './Studio'
import {val} from '@theatre/dataverse' import {val} from '@theatre/dataverse'
import {getMounter} from './utils/renderInPortalInContext' import {getMounter} from './utils/renderInPortalInContext'
import {Toolbar} from './toolbars/GlobalToolbar/GlobalToolbar'
import {withStyledShadow} from './css' import {withStyledShadow} from './css'
import ExtensionToolbar from './toolbars/ExtensionToolbar/ExtensionToolbar'
export default class UI { export default class UI {
readonly containerEl = document.createElement('div') readonly containerEl = document.createElement('div')
@ -93,7 +93,11 @@ export default class UI {
renderToolset(toolsetId: string, htmlNode: HTMLElement) { renderToolset(toolsetId: string, htmlNode: HTMLElement) {
const s = getMounter() const s = getMounter()
s.mountOrRender(withStyledShadow(Toolbar), {toolbarId: toolsetId}, htmlNode) s.mountOrRender(
withStyledShadow(ExtensionToolbar),
{toolbarId: toolsetId},
htmlNode,
)
return s.unmount return s.unmount
} }

View file

@ -4,7 +4,7 @@ import {val} from '@theatre/dataverse'
import React, {useEffect} from 'react' import React, {useEffect} from 'react'
import styled, {createGlobalStyle} from 'styled-components' import styled, {createGlobalStyle} from 'styled-components'
import PanelsRoot from './PanelsRoot' import PanelsRoot from './PanelsRoot'
import GlobalToolbar from '@theatre/studio/toolbars/GlobalToolbar/GlobalToolbar' import GlobalToolbar from '@theatre/studio/toolbars/GlobalToolbar'
import useRefAndState from '@theatre/studio/utils/useRefAndState' import useRefAndState from '@theatre/studio/utils/useRefAndState'
import {PortalContext} from 'reakit' import {PortalContext} from 'reakit'
import type {$IntentionalAny} from '@theatre/shared/utils/types' import type {$IntentionalAny} from '@theatre/shared/utils/types'

View file

@ -1,6 +1,6 @@
import {getOutlineSelection} from '@theatre/studio/selectors' import {getOutlineSelection} from '@theatre/studio/selectors'
import {usePrism} from '@theatre/react' import {usePrism, useVal} from '@theatre/react'
import React from 'react' import React, {useEffect, useLayoutEffect} from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import {isProject, isSheetObject} from '@theatre/shared/instanceTypes' import {isProject, isSheetObject} from '@theatre/shared/instanceTypes'
import { import {
@ -11,46 +11,35 @@ import {
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css' import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
import ObjectDetails from './ObjectDetails' import ObjectDetails from './ObjectDetails'
import ProjectDetails from './ProjectDetails' import ProjectDetails from './ProjectDetails'
import getStudio from '@theatre/studio/getStudio'
import useHotspot from '@theatre/studio/uiComponents/useHotspot'
import {Box, prism, val} from '@theatre/dataverse'
const Container = styled.div` const headerHeight = `32px`
background-color: transparent;
pointer-events: none; const Container = styled.div<{pin: boolean}>`
background-color: rgba(40, 43, 47, 0.8);
position: fixed; position: fixed;
left: 0; right: 8px;
right: 0; top: 50px;
top: 12px; width: 236px;
bottom: 0px; height: fit-content;
z-index: ${panelZIndexes.propsPanel}; z-index: ${panelZIndexes.propsPanel};
&:before { box-shadow: 0 1px 1px rgba(0, 0, 0, 0.25), 0 2px 6px rgba(0, 0, 0, 0.15);
backdrop-filter: blur(14px);
border-radius: 2px;
display: ${({pin}) => (pin ? 'block' : 'none')};
&:hover {
display: block; display: block;
content: ' ';
position: absolute;
top: 0;
bottom: 0;
right: 0;
width: 20px;
${pointerEventsAutoInNormalMode};
}
`
const Content = styled.div`
position: absolute;
top: 0;
right: 0;
width: 260px;
bottom: 0;
/* transform: translateX(100%); */
/* pointer-events: none; */
${Container}:hover & {
transform: translateX(0);
} }
` `
const Title = styled.div` const Title = styled.div`
margin: 0 10px; margin: 0 10px;
color: #ffffffc2; color: #919191;
font-weight: 500; font-weight: 500;
font-size: 10px; font-size: 10px;
user-select: none; user-select: none;
@ -60,37 +49,15 @@ const Title = styled.div`
text-overflow: ellipsis; text-overflow: ellipsis;
` `
const headerHeight = `32px`
const Header = styled.div` const Header = styled.div`
height: ${headerHeight}; height: ${headerHeight};
display: flex; display: flex;
align-items: center; align-items: center;
position: absolute;
top: 0;
left: 0;
right: 0;
&:after {
position: absolute;
inset: 1px 0px;
display: block;
content: ' ';
pointer-events: none;
z-index: -1;
background-color: #262c2dd1;
/* border-radius: 2px 0 0 2px; */
}
` `
const Body = styled.div` const Body = styled.div`
${pointerEventsAutoInNormalMode}; ${pointerEventsAutoInNormalMode};
position: absolute; max-height: calc(100vh - 100px);
top: ${headerHeight};
left: 0;
right: 0;
height: auto;
max-height: calc(100% - ${headerHeight});
overflow-y: scroll; overflow-y: scroll;
&::-webkit-scrollbar { &::-webkit-scrollbar {
display: none; display: none;
@ -102,56 +69,87 @@ const Body = styled.div`
` `
const DetailPanel: React.FC<{}> = (props) => { const DetailPanel: React.FC<{}> = (props) => {
const pin = useVal(getStudio().atomP.ahistoric.pinDetails) !== false
const hostspotActive = useHotspot('right')
useLayoutEffect(() => {
isDetailPanelHotspotActiveB.set(hostspotActive)
}, [hostspotActive])
// cleanup
useEffect(() => {
return () => {
isDetailPanelHoveredB.set(false)
isDetailPanelHotspotActiveB.set(false)
}
}, [])
return usePrism(() => { return usePrism(() => {
const selection = getOutlineSelection() const selection = getOutlineSelection()
const obj = selection.find(isSheetObject) const obj = selection.find(isSheetObject)
if (obj) { if (obj) {
return ( return (
<Container> <Container
<Content data-testid="DetailPanel-Object"> data-testid="DetailPanel-Object"
<Header> pin={pin || hostspotActive}
<Title onMouseEnter={() => {
title={`${obj.sheet.address.sheetId}: ${obj.sheet.address.sheetInstanceId} > ${obj.address.objectKey}`} isDetailPanelHoveredB.set(true)
> }}
<TitleBar_Piece>{obj.sheet.address.sheetId} </TitleBar_Piece> onMouseLeave={() => {
isDetailPanelHoveredB.set(false)
}}
>
<Header>
<Title
title={`${obj.sheet.address.sheetId}: ${obj.sheet.address.sheetInstanceId} > ${obj.address.objectKey}`}
>
<TitleBar_Piece>{obj.sheet.address.sheetId} </TitleBar_Piece>
<TitleBar_Punctuation>{':'}&nbsp;</TitleBar_Punctuation> <TitleBar_Punctuation>{':'}&nbsp;</TitleBar_Punctuation>
<TitleBar_Piece> <TitleBar_Piece>
{obj.sheet.address.sheetInstanceId}{' '} {obj.sheet.address.sheetInstanceId}{' '}
</TitleBar_Piece> </TitleBar_Piece>
<TitleBar_Punctuation>&nbsp;{'>'}&nbsp;</TitleBar_Punctuation> <TitleBar_Punctuation>&nbsp;&rarr;&nbsp;</TitleBar_Punctuation>
<TitleBar_Piece>{obj.address.objectKey}</TitleBar_Piece> <TitleBar_Piece>{obj.address.objectKey}</TitleBar_Piece>
</Title> </Title>
</Header> </Header>
<Body> <Body>
<ObjectDetails objects={[obj]} /> <ObjectDetails objects={[obj]} />
</Body> </Body>
</Content>
</Container> </Container>
) )
} }
const project = selection.find(isProject) const project = selection.find(isProject)
if (project) { if (project) {
return ( return (
<Container> <Container pin={pin || hostspotActive}>
<Content> <Header>
<Header> <Title title={`${project.address.projectId}`}>
<Title title={`${project.address.projectId}`}> <TitleBar_Piece>{project.address.projectId} </TitleBar_Piece>
<TitleBar_Piece>{project.address.projectId} </TitleBar_Piece> </Title>
</Title> </Header>
</Header> <Body>
<Body> <ProjectDetails projects={[project]} />
<ProjectDetails projects={[project]} /> </Body>
</Body>
</Content>
</Container> </Container>
) )
} }
return <></> return <></>
}, []) }, [pin, hostspotActive])
} }
export default DetailPanel export default DetailPanel
const isDetailPanelHotspotActiveB = new Box<boolean>(false)
const isDetailPanelHoveredB = new Box<boolean>(false)
export const shouldShowDetailD = prism<boolean>(() => {
const isHovered = val(isDetailPanelHoveredB.derivation)
const isHotspotActive = val(isDetailPanelHotspotActiveB.derivation)
return isHovered || isHotspotActive
})

View file

@ -6,10 +6,7 @@ import last from 'lodash-es/last'
import {darken, transparentize} from 'polished' import {darken, transparentize} from 'polished'
import React from 'react' import React from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import { import {indentationFormula} from '@theatre/studio/panels/DetailPanel/DeterminePropEditorForDetail/SingleRowPropEditor'
indentationFormula,
rowBg,
} from '@theatre/studio/panels/DetailPanel/DeterminePropEditorForDetail/SingleRowPropEditor'
import {propNameTextCSS} from '@theatre/studio/propEditors/utils/propNameTextCSS' import {propNameTextCSS} from '@theatre/studio/propEditors/utils/propNameTextCSS'
import DefaultOrStaticValueIndicator from '@theatre/studio/propEditors/DefaultValueIndicator' import DefaultOrStaticValueIndicator from '@theatre/studio/propEditors/DefaultValueIndicator'
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css' import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
@ -29,8 +26,6 @@ const Header = styled.div`
display: flex; display: flex;
align-items: stretch; align-items: stretch;
position: relative; position: relative;
${rowBg};
` `
const Padding = styled.div` const Padding = styled.div`

View file

@ -6,39 +6,12 @@ import useRefAndState from '@theatre/studio/utils/useRefAndState'
import {last} from 'lodash-es' import {last} from 'lodash-es'
import React from 'react' import React from 'react'
import type {useEditingToolsForSimplePropInDetailsPanel} from '@theatre/studio/propEditors/useEditingToolsForSimpleProp' import type {useEditingToolsForSimplePropInDetailsPanel} from '@theatre/studio/propEditors/useEditingToolsForSimpleProp'
import styled, {css} from 'styled-components' import styled from 'styled-components'
import {transparentize} from 'polished'
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css' import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
import {propNameTextCSS} from '@theatre/studio/propEditors/utils/propNameTextCSS' import {propNameTextCSS} from '@theatre/studio/propEditors/utils/propNameTextCSS'
export const indentationFormula = `calc(var(--left-pad) + var(--depth) * var(--step))` export const indentationFormula = `calc(var(--left-pad) + var(--depth) * var(--step))`
export const rowBgColor = transparentize(0.05, '#282b2f')
export const rowBg = css`
&:after,
&:before {
position: absolute;
display: block;
content: ' ';
z-index: -1;
box-sizing: content-box;
}
&:after {
inset: 0px 0 1px calc(-2px + var(--left-pad) + var(--depth) * var(--step));
background-color: ${rowBgColor};
}
&:before {
height: 2px;
right: 0;
bottom: 0px;
left: calc(-2px + var(--left-pad) + var(--depth) * var(--step));
background-color: ${transparentize(0.2, rowBgColor)};
}
`
const LeftRow = styled.div` const LeftRow = styled.div`
display: flex; display: flex;
height: 30px; height: 30px;
@ -47,8 +20,6 @@ const LeftRow = styled.div`
--right-width: 60%; --right-width: 60%;
position: relative; position: relative;
${pointerEventsAutoInNormalMode}; ${pointerEventsAutoInNormalMode};
${rowBg};
` `
const Left = styled.div` const Left = styled.div`

View file

@ -5,12 +5,9 @@ import usePopover from '@theatre/studio/uiComponents/Popover/usePopover'
import React, {useCallback, useState} from 'react' import React, {useCallback, useState} from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import DetailPanelButton from '@theatre/studio/uiComponents/DetailPanelButton' import DetailPanelButton from '@theatre/studio/uiComponents/DetailPanelButton'
import {rowBgColor} from './DeterminePropEditorForDetail/SingleRowPropEditor'
import StateConflictRow from './ProjectDetails/StateConflictRow' import StateConflictRow from './ProjectDetails/StateConflictRow'
const Container = styled.div` const Container = styled.div``
background-color: ${rowBgColor};
`
const TheExportRow = styled.div` const TheExportRow = styled.div`
padding: 8px 10px; padding: 8px 10px;

View file

@ -1,9 +1,9 @@
import type {VoidFn} from '@theatre/shared/utils/types' import type {VoidFn} from '@theatre/shared/utils/types'
import React from 'react' import React from 'react'
import {GoChevronRight, DiHtml53DEffects} from 'react-icons/all'
import styled, {css} from 'styled-components' import styled, {css} from 'styled-components'
import noop from '@theatre/shared/utils/noop' import noop from '@theatre/shared/utils/noop'
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css' import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
import {ChevronRight, Package} from '@theatre/studio/uiComponents/icons'
export const Container = styled.li` export const Container = styled.li`
margin: 0; margin: 0;
@ -17,14 +17,16 @@ export const Container = styled.li`
export const BaseHeader = styled.div`` export const BaseHeader = styled.div``
const baseBg = `#3e4447`
const baseBorderColor = `#34343e`
const Header = styled(BaseHeader)` const Header = styled(BaseHeader)`
padding-left: calc(4px + var(--depth) * 16px); position: relative;
margin-top: 2px;
margin-bottom: 2px;
margin-left: calc(4px + var(--depth) * 16px);
padding-left: 8px;
padding-right: 8px; padding-right: 8px;
height: 28px; gap: 8px;
height: 23px;
line-height: 0;
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
@ -32,29 +34,42 @@ const Header = styled(BaseHeader)`
pointer-events: none; pointer-events: none;
white-space: nowrap; white-space: nowrap;
color: rgba(255, 255, 255, 0.75); border-radius: 2px;
--item-bg: ${baseBg}; box-shadow: 0 3px 4px -1px rgba(0, 0, 0, 0.48);
--item-border-color: ${baseBorderColor};
color: rgba(255, 255, 255, 0.9);
background: rgba(40, 43, 47, 0.65);
backdrop-filter: blur(14px);
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
&.descendant-is-selected { &.descendant-is-selected {
color: rgba(255, 255, 255, 0.9); background: rgba(29, 53, 59, 0.7);
--item-bg: #2e4244ed;
--item-border-color: #254355;
} }
&:not(.not-selectable):hover { &:not(.not-selectable):not(.selected):hover {
color: #fff; background: rgba(59, 63, 69, 0.9);
--item-bg: #1e5866; border-bottom: 1px solid rgba(255, 255, 255, 0.24);
--item-border-color: #152f42; }
&:not(.not-selectable):not(.selected):active {
background: rgba(82, 88, 96, 0.9);
border-bottom: 1px solid rgba(255, 255, 255, 0.24);
} }
&.selected { &.selected {
color: rgba(255, 255, 255, 0.9); background: rgba(30, 88, 102, 0.7);
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
--item-bg: #1e5866; // Hit zone
--item-border-color: #152f42; &:before {
position: absolute;
inset: -1px -20px;
display: block;
content: ' ';
z-index: 5;
${pointerEventsAutoInNormalMode};
} }
` `
@ -68,61 +83,23 @@ export const outlineItemFont = css`
const Head_Label = styled.span` const Head_Label = styled.span`
${outlineItemFont}; ${outlineItemFont};
padding: 2px 8px;
${pointerEventsAutoInNormalMode}; ${pointerEventsAutoInNormalMode};
position: relative; position: relative;
// Compensate for border bottom
top: 1px;
display: flex; display: flex;
height: 17px; height: 20px;
align-items: center; align-items: center;
box-sizing: border-box;
background-color: var(--item-bg);
&:after {
border: 1px solid var(--item-border-color);
position: absolute;
inset: 0px;
display: block;
content: ' ';
z-index: -1;
pointer-events: none;
border-radius: 2px;
box-sizing: border-box;
box-shadow: 0px 3px 4px -1px rgba(0, 0, 0, 0.48);
}
// hit-zone
&:before {
position: absolute;
inset: -1px -20px;
display: block;
content: ' ';
z-index: 0;
${pointerEventsAutoInNormalMode};
}
` `
const Head_IconContainer = styled.span` const Head_IconContainer = styled.div`
width: 18px;
box-sizing: border-box;
height: 18px;
margin-right: 4px;
font-weight: 500; font-weight: 500;
display: flex; display: flex;
align-items: center;
justify-content: center; justify-content: center;
align-items: center;
position: relative; position: relative;
opacity: 0.99; opacity: 0.99;
&:after {
display: block;
content: ' ';
position: absolute;
inset: 0px;
z-index: -1;
background-color: var(--item-bg);
opacity: 0.6;
border-radius: 2px;
}
` `
const Head_Icon_WithDescendants = styled.span<{isOpen: boolean}>` const Head_Icon_WithDescendants = styled.span<{isOpen: boolean}>`
@ -160,14 +137,14 @@ const BaseItem: React.FC<{
{'--depth': depth} {'--depth': depth}
} }
> >
<Header className={selectionStatus} onClick={select ?? noop}> <Header className={selectionStatus} onClick={select ?? noop} data-header>
<Head_IconContainer> <Head_IconContainer>
{canContainChildren ? ( {canContainChildren ? (
<Head_Icon_WithDescendants isOpen={true}> <Head_Icon_WithDescendants isOpen={true}>
<GoChevronRight /> <ChevronRight />
</Head_Icon_WithDescendants> </Head_Icon_WithDescendants>
) : ( ) : (
<DiHtml53DEffects /> <Package />
)} )}
</Head_IconContainer> </Head_IconContainer>

View file

@ -1,188 +1,71 @@
import React from 'react' import React, {useEffect, useState} from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import {panelZIndexes} from '@theatre/studio/panels/BasePanel/common' import {panelZIndexes} from '@theatre/studio/panels/BasePanel/common'
import ProjectsList from './ProjectsList/ProjectsList' 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 {useVal} from '@theatre/react'
import {VscListTree} from 'react-icons/all'
import {usePrism} from '@theatre/react'
import getStudio from '@theatre/studio/getStudio' import getStudio from '@theatre/studio/getStudio'
import {val} from '@theatre/dataverse' import useHotspot from '@theatre/studio/uiComponents/useHotspot'
import useTooltip from '@theatre/studio/uiComponents/Popover/useTooltip'
import BasicTooltip from '@theatre/studio/uiComponents/Popover/BasicTooltip'
import ErrorTooltip from '@theatre/studio/uiComponents/Popover/ErrorTooltip'
const Container = styled.div` const headerHeight = `44px`
const Container = styled.div<{pin: boolean}>`
background-color: transparent; background-color: transparent;
pointer-events: none;
position: absolute; position: absolute;
left: 0;
top: 12px;
bottom: 0px;
right: 0;
z-index: ${panelZIndexes.outlinePanel};
&:before {
display: block;
content: ' ';
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 40px;
${pointerEventsAutoInNormalMode};
}
&:hover:before {
top: -12px;
width: 300px;
}
`
const TriggerContainer = styled.div`
margin-left: 12px;
position: relative;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
`
const Content = styled.div`
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
transform: translateX(-100%);
pointer-events: none;
${Container}:hover & {
transform: translateX(0);
}
`
const headerHeight = `32px`
const TriggerButton = styled(ToolbarIconButton)`
${Container}:hover & {
background-color: rgba(36, 38, 42, 0.95);
&:after {
border-color: rgba(255, 255, 255, 0.22);
}
color: white;
}
`
const Title = styled.div`
margin: 0 12px;
color: #ffffffc2;
font-weight: 500;
font-size: 10px;
user-select: none;
position: relative;
display: none;
background-color: rgba(60, 60, 60, 0.2);
height: 24px;
${Container}:hover & {
display: block;
}
&:after {
position: absolute;
inset: 4px 0px;
display: block;
content: ' ';
pointer-events: none;
z-index: -1;
background-color: #69777947;
border-radius: 0 2px 2px 0;
}
`
const Body = styled.div`
${pointerEventsAutoInNormalMode};
position: absolute;
top: ${headerHeight};
left: 8px; left: 8px;
height: auto; z-index: ${panelZIndexes.outlinePanel};
${pointerEventsAutoInNormalMode};
top: calc(${headerHeight} + 8px);
height: fit-content;
max-height: calc(100% - ${headerHeight}); max-height: calc(100% - ${headerHeight});
overflow-y: scroll; overflow-y: scroll;
overflow-x: hidden; overflow-x: hidden;
padding: 0; padding: 0;
user-select: none; user-select: none;
&::-webkit-scrollbar { &::-webkit-scrollbar {
display: none; display: none;
} }
scrollbar-width: none; scrollbar-width: none;
`
const NumberOfConflictsIndicator = styled.div` display: ${({pin}) => (pin ? 'block' : 'none')};
color: white;
width: 14px; &:hover {
height: 14px; display: block;
background: #d00; }
border-radius: 4px;
text-align: center; // Create a small buffer on the bottom to aid selecting the bottom item in a long, scrolling list
line-height: 14px; &::after {
font-weight: 600; content: '';
font-size: 8px; display: block;
position: relative; height: 20px;
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 pin = useVal(getStudio().atomP.ahistoric.pinOutline) !== false
const ephemeralStateOfAllProjects = val( const show = useVal(getStudio().atomP.ephemeral.showOutline)
getStudio().atomP.ephemeral.coreByProject, const active = useHotspot('left')
) const [hovered, setHovered] = useState(false)
return Object.entries(ephemeralStateOfAllProjects)
.map(([projectId, state]) => ({projectId, state}))
.filter(
({state}) =>
state.loadingState.type === 'browserStateIsNotBasedOnDiskState',
)
}, [])
const [triggerTooltip, triggerButtonRef] = useTooltip<HTMLButtonElement>( useEffect(() => {
{enabled: conflicts.length > 0, enterDelay: conflicts.length > 0 ? 0 : 200}, getStudio().transaction(({stateEditors, drafts}) => {
() => stateEditors.studio.ephemeral.setShowOutline(active || hovered)
conflicts.length > 0 ? ( })
<ErrorTooltip> }, [active, hovered])
{conflicts.length === 1
? `There is a state conflict in project "${conflicts[0].projectId}". Select the project in the outline below in order to fix it.`
: `There are ${conflicts.length} projects that have state conflicts. They are highlighted in the outline below. `}
</ErrorTooltip>
) : (
<BasicTooltip>Outline</BasicTooltip>
),
)
return ( return (
<Container> <Container
<TriggerContainer> pin={pin || show}
{triggerTooltip} onMouseEnter={() => {
<TriggerButton setHovered(true)
ref={triggerButtonRef} }}
data-testid="OutlinePanel-TriggerButton" onMouseLeave={() => {
> setHovered(false)
<VscListTree /> }}
</TriggerButton> >
{conflicts.length > 0 ? ( <ProjectsList />
<NumberOfConflictsIndicator>
{conflicts.length}
</NumberOfConflictsIndicator>
) : null}
{/* <Title>Outline</Title> */}
</TriggerContainer>
<Content>
<Body data-testid="OutlinePanel-Content">
<ProjectsList />
</Body>
</Content>
</Container> </Container>
) )
} }

View file

@ -33,7 +33,7 @@ const ColorPreviewPuck = styled.div.attrs<ColorPreviewPuckProps>((props) => ({
}))<ColorPreviewPuckProps>` }))<ColorPreviewPuckProps>`
height: 18px; height: 18px;
aspect-ratio: 1; aspect-ratio: 1;
border-radius: 2px; border-radius: 99999px;
` `
const HexInput = styled(BasicStringInput)` const HexInput = styled(BasicStringInput)`
@ -46,14 +46,13 @@ const RgbaPopover = styled.div`
position: absolute; position: absolute;
background-color: ${popoverBackgroundColor}; background-color: ${popoverBackgroundColor};
color: white; color: white;
padding: 0;
margin: 0; margin: 0;
cursor: default; cursor: default;
border-radius: 3px; border-radius: 3px;
z-index: 10000; z-index: 10000;
backdrop-filter: blur(8px); backdrop-filter: blur(8px);
padding: 4; padding: 4px;
pointer-events: all; pointer-events: all;
border: none; border: none;

View file

@ -3,6 +3,6 @@ import {css} from 'styled-components'
export const propNameTextCSS = css` export const propNameTextCSS = css`
font-weight: 300; font-weight: 300;
font-size: 11px; font-size: 11px;
color: #9a9a9a; color: #919191;
text-shadow: 0.5px 0.5px 2px rgba(0, 0, 0, 0.3); text-shadow: 0.5px 0.5px 2px rgba(0, 0, 0, 0.3);
` `

View file

@ -41,6 +41,7 @@ const initialState: StudioState = {
byId: {}, byId: {},
paneClasses: {}, paneClasses: {},
}, },
showOutline: false,
}, },
} }

View file

@ -42,6 +42,7 @@ import type {
OutlineSelectionState, OutlineSelectionState,
PanelPosition, PanelPosition,
StudioAhistoricState, StudioAhistoricState,
StudioEphemeralState,
StudioHistoricStateSequenceEditorMarker, StudioHistoricStateSequenceEditorMarker,
} from './types' } from './types'
import {clamp, uniq} from 'lodash-es' import {clamp, uniq} from 'lodash-es'
@ -339,6 +340,11 @@ namespace stateEditors {
} }
} }
export namespace ephemeral { export namespace ephemeral {
export function setShowOutline(
showOutline: StudioEphemeralState['showOutline'],
) {
drafts().ephemeral.showOutline = showOutline
}
export namespace projects { export namespace projects {
export namespace stateByProjectId { export namespace stateByProjectId {
export function _ensure(p: ProjectAddress) { export function _ensure(p: ProjectAddress) {
@ -400,6 +406,16 @@ namespace stateEditors {
} }
} }
export namespace ahistoric { export namespace ahistoric {
export function setPinOutline(
pinOutline: StudioAhistoricState['pinOutline'],
) {
drafts().ahistoric.pinOutline = pinOutline
}
export function setPinDetails(
pinDetails: StudioAhistoricState['pinDetails'],
) {
drafts().ahistoric.pinDetails = pinDetails
}
export function setVisibilityState( export function setVisibilityState(
visibilityState: StudioAhistoricState['visibilityState'], visibilityState: StudioAhistoricState['visibilityState'],
) { ) {

View file

@ -6,6 +6,14 @@ import type {PointableSet} from '@theatre/shared/utils/PointableSet'
import type {StudioSheetItemKey} from '@theatre/shared/utils/ids' import type {StudioSheetItemKey} from '@theatre/shared/utils/ids'
export type StudioAhistoricState = { export type StudioAhistoricState = {
/**
* undefined means the outline menu is pinned
*/
pinOutline?: boolean
/**
* undefined means the detail panel is pinned
*/
pinDetails?: boolean
visibilityState: 'everythingIsHidden' | 'everythingIsVisible' visibilityState: 'everythingIsHidden' | 'everythingIsVisible'
clipboard?: { clipboard?: {
keyframes?: Keyframe[] keyframes?: Keyframe[]

View file

@ -5,6 +5,26 @@ import type {
PaneClassDefinition, PaneClassDefinition,
} from '@theatre/studio/TheatreStudio' } from '@theatre/studio/TheatreStudio'
/**
* Technically, all parts of the ephemeral state can be implemented
* outside the store, using simple Box|Atom of dataverse.
*
* The only reason that _some_ of these cases reside in StudioEphemeralState,
* is to bring them into attention, because these pieces of the state are useful
* in several (3+) places in the application.
*
* Note: Should we just implement all of ephemeral state as boxes and atoms,
* and remove ephemeral state from the store?
* - We'd still have to namespace and organize these pieces of ephemeral state,
* so they're discoverable.
*
* Disadvantage of that:
* - We may want to send over the wire pieces the ephemeral state that other users
* have interest in. For example, if Alice is dragging Planet.position, Bob would
* want to observe the drag, and not just its final state, which would be in the historic
* state. (still, ephemeral state would never be persisted, but parts of it could be sent
* over the wire).
*/
export type StudioEphemeralState = { export type StudioEphemeralState = {
initialised: boolean initialised: boolean
coreByProject: {[projectId in string]: ProjectState['ephemeral']} coreByProject: {[projectId in string]: ProjectState['ephemeral']}
@ -35,4 +55,5 @@ export type StudioEphemeralState = {
} }
} }
} }
showOutline: boolean
} }

View file

@ -3,21 +3,15 @@ import {useVal} from '@theatre/react'
import type {IExtension} from '@theatre/studio' import type {IExtension} from '@theatre/studio'
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css' import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
import getStudio from '@theatre/studio/getStudio' import getStudio from '@theatre/studio/getStudio'
import {panelZIndexes} from '@theatre/studio/panels/BasePanel/common'
import type {ToolsetConfig} from '@theatre/studio/TheatreStudio' import type {ToolsetConfig} from '@theatre/studio/TheatreStudio'
import React, {useLayoutEffect, useMemo} from 'react' import React, {useLayoutEffect, useMemo} from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import Toolset from './Toolset' import Toolset from './Toolset'
const Container = styled.div` const Container = styled.div`
position: fixed;
z-index: ${panelZIndexes.toolbar};
top: 12px;
right: 12px;
left: 12px;
height: 36px; height: 36px;
pointer-events: none; /* pointer-events: none; */
display: flex; display: flex;
gap: 1rem; gap: 1rem;
@ -60,17 +54,9 @@ const ExtensionToolsetRender: React.FC<{
return <Toolset config={config} /> return <Toolset config={config} />
} }
const GlobalToolbar: React.FC<{}> = (props) => { export const ExtensionToolbar: React.FC<{toolbarId: string}> = ({
return ( toolbarId,
<Container> }) => {
<Bg>
<Toolbar toolbarId="global" />
</Bg>
</Container>
)
}
export const Toolbar: React.FC<{toolbarId: string}> = ({toolbarId}) => {
const groups: Array<React.ReactNode> = [] const groups: Array<React.ReactNode> = []
const extensionsById = useVal(getStudio().atomP.ephemeral.extensions.byId) const extensionsById = useVal(getStudio().atomP.ephemeral.extensions.byId)
@ -87,7 +73,8 @@ export const Toolbar: React.FC<{toolbarId: string}> = ({toolbarId}) => {
} }
if (groups.length === 0) return null if (groups.length === 0) return null
return <>{groups}</>
return <Container>{groups}</Container>
} }
export default GlobalToolbar export default ExtensionToolbar

View file

@ -0,0 +1,130 @@
import {usePrism, useVal} from '@theatre/react'
import getStudio from '@theatre/studio/getStudio'
import React from 'react'
import styled from 'styled-components'
import type {$IntentionalAny} from '@theatre/dataverse/dist/types'
import useTooltip from '@theatre/studio/uiComponents/Popover/useTooltip'
import ErrorTooltip from '@theatre/studio/uiComponents/Popover/ErrorTooltip'
import BasicTooltip from '@theatre/studio/uiComponents/Popover/BasicTooltip'
import {val} from '@theatre/dataverse'
import ExtensionToolbar from './ExtensionToolbar/ExtensionToolbar'
import PinButton from './PinButton'
import {
ChevronLeft,
ChevronRight,
Details,
Outline,
} from '@theatre/studio/uiComponents/icons'
import {shouldShowDetailD} from '@theatre/studio/panels/DetailPanel/DetailPanel'
const Container = styled.div`
height: 36px;
pointer-events: none;
display: flex;
justify-content: space-between;
padding: 12px;
`
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 SubContainer = styled.div`
display: flex;
gap: 8px;
`
const GlobalToolbar: React.FC = () => {
const conflicts = usePrism(() => {
const ephemeralStateOfAllProjects = val(
getStudio().atomP.ephemeral.coreByProject,
)
return Object.entries(ephemeralStateOfAllProjects)
.map(([projectId, state]) => ({projectId, state}))
.filter(
({state}) =>
state.loadingState.type === 'browserStateIsNotBasedOnDiskState',
)
}, [])
const [triggerTooltip, triggerButtonRef] = useTooltip(
{enabled: conflicts.length > 0, enterDelay: conflicts.length > 0 ? 0 : 200},
() =>
conflicts.length > 0 ? (
<ErrorTooltip>
{conflicts.length === 1
? `There is a state conflict in project "${conflicts[0].projectId}". Select the project in the outline below in order to fix it.`
: `There are ${conflicts.length} projects that have state conflicts. They are highlighted in the outline below. `}
</ErrorTooltip>
) : (
<BasicTooltip>Outline</BasicTooltip>
),
)
const outlinePinned = useVal(getStudio().atomP.ahistoric.pinOutline)
const detailsPinned = useVal(getStudio().atomP.ahistoric.pinDetails)
const showOutline = useVal(getStudio().atomP.ephemeral.showOutline)
const showDetails = useVal(shouldShowDetailD)
return (
<Container>
<SubContainer>
{triggerTooltip}
<PinButton
ref={triggerButtonRef as $IntentionalAny}
data-testid="OutlinePanel-TriggerButton"
onClick={() => {
getStudio().transaction(({stateEditors, drafts}) => {
stateEditors.studio.ahistoric.setPinOutline(
!drafts.ahistoric.pinOutline,
)
})
}}
icon={<Outline />}
pinHintIcon={<ChevronRight />}
unpinHintIcon={<ChevronLeft />}
pinned={outlinePinned}
hint={showOutline}
/>
{conflicts.length > 0 ? (
<NumberOfConflictsIndicator>
{conflicts.length}
</NumberOfConflictsIndicator>
) : null}
<ExtensionToolbar toolbarId="global" />
</SubContainer>
<SubContainer>
<PinButton
ref={triggerButtonRef as $IntentionalAny}
onClick={() => {
getStudio().transaction(({stateEditors, drafts}) => {
stateEditors.studio.ahistoric.setPinDetails(
!drafts.ahistoric.pinDetails,
)
})
}}
icon={<Details />}
pinHintIcon={<ChevronLeft />}
unpinHintIcon={<ChevronRight />}
pinned={detailsPinned}
hint={showDetails}
/>
</SubContainer>
</Container>
)
}
export default GlobalToolbar

View file

@ -0,0 +1,69 @@
import styled from 'styled-components'
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
import type {ComponentPropsWithRef, ReactNode} from 'react';
import React, { forwardRef} from 'react'
const Container = styled.button<{pinned?: boolean}>`
${pointerEventsAutoInNormalMode};
position: relative;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 600;
width: 32px;
height: 32px;
outline: none;
color: #a8a8a9;
background: ${({pinned}) =>
pinned ? 'rgba(40, 43, 47, 0.9)' : 'rgba(40, 43, 47, 0.45)'};
backdrop-filter: blur(14px);
border: none;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
&:hover {
background: rgba(59, 63, 69, 0.8);
}
&:active {
background: rgba(82, 88, 96, 0.8);
}
@supports not (backdrop-filter: blur()) {
background: rgba(40, 43, 47, 0.8);
&:hover {
background: rgba(59, 63, 69, 0.8);
}
&:active {
background: rgba(82, 88, 96, 0.7);
}
&.selected {
background: rgb(27, 32, 35);
}
}
`
interface PinButtonProps extends ComponentPropsWithRef<'button'> {
icon: ReactNode
pinHintIcon: ReactNode
unpinHintIcon: ReactNode
hint?: boolean
pinned?: boolean
}
const PinButton = forwardRef<HTMLButtonElement, PinButtonProps>(
({hint, pinned, icon, pinHintIcon, unpinHintIcon, ...props}, ref) => {
return (
<Container {...props} pinned={pinned} ref={ref}>
{hint && !pinned ? pinHintIcon : hint && pinned ? unpinHintIcon : icon}
</Container>
)
},
)
export default PinButton

View file

@ -0,0 +1,22 @@
import * as React from 'react'
function ArrowClockwise(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
width={16}
height={16}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M5.586 3.38a5 5 0 015.45 1.087L12.574 6h-1.572a.5.5 0 000 1h3a.5.5 0 00.5-.5v-3a.5.5 0 10-1 0v2.013l-1.76-1.754a6 6 0 100 8.482.5.5 0 10-.707-.707A5 5 0 115.587 3.38z"
fill="#fff"
fillOpacity={0.6}
/>
</svg>
)
}
export default ArrowClockwise

View file

@ -0,0 +1,22 @@
import * as React from 'react'
function ArrowsOutCardinal(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
width={16}
height={16}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M8 1a.498.498 0 01.358.15l1.764 1.765a.5.5 0 01-.707.707L8.5 2.707V6a.5.5 0 01-1 0V2.707l-.915.915a.5.5 0 11-.707-.707l1.768-1.769A.498.498 0 018 1zM8 9.5a.5.5 0 01.5.5v3.292l.915-.915a.5.5 0 01.707.707L8.37 14.836a.499.499 0 01-.74.001l-1.752-1.753a.5.5 0 01.707-.707l.915.915V10a.5.5 0 01.5-.5zM3.622 6.584a.5.5 0 10-.707-.707L1.146 7.646a.498.498 0 00.018.724l1.751 1.752a.5.5 0 10.707-.708L2.708 8.5H6a.5.5 0 000-1H2.706l.916-.916zM12.378 5.877a.5.5 0 01.707 0l1.768 1.769a.498.498 0 01-.017.724l-1.751 1.752a.5.5 0 01-.707-.708l.914-.914H10a.5.5 0 010-1h3.294l-.916-.916a.5.5 0 010-.707z"
fill="#fff"
fillOpacity={0.6}
/>
</svg>
)
}
export default ArrowsOutCardinal

View file

@ -0,0 +1,31 @@
import * as React from 'react'
function Camera(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
width={16}
height={16}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M7.767 5.75a2.75 2.75 0 100 5.5 2.75 2.75 0 000-5.5zM6.017 8.5a1.75 1.75 0 113.5 0 1.75 1.75 0 01-3.5 0z"
fill="#fff"
fillOpacity={0.6}
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M5.773 2.25a.5.5 0 00-.416.223l-.85 1.277H2.782a1.496 1.496 0 00-1.497 1.5v7a1.501 1.501 0 001.497 1.5h9.972a1.496 1.496 0 001.498-1.5v-7a1.501 1.501 0 00-1.498-1.5h-1.726l-.849-1.277a.5.5 0 00-.416-.223H5.773zm-.58 2.277L6.04 3.25h3.453l.849 1.277a.5.5 0 00.416.223h1.994a.496.496 0 01.498.5v7a.501.501 0 01-.498.5H2.781a.495.495 0 01-.497-.5v-7a.501.501 0 01.497-.5h1.995a.5.5 0 00.416-.223z"
fill="#fff"
fillOpacity={0.6}
/>
</svg>
)
}
export default Camera

View file

@ -0,0 +1,24 @@
import * as React from 'react'
function ChevronDown(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
width={16}
height={16}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8 10.5L4 6.654 5.2 5.5 8 8.385 10.8 5.5 12 6.654 8 10.5z"
fill="#fff"
fillOpacity={0.6}
/>
</svg>
)
}
export default ChevronDown

View file

@ -0,0 +1,22 @@
import * as React from 'react'
function ChevronLeft(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
width={16}
height={16}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M10.45 2.266l.956.954-4.763 4.763 4.763 4.762-.955.954-5.712-5.716 5.712-5.717z"
fill="#fff"
fillOpacity={0.6}
/>
</svg>
)
}
export default ChevronLeft

View file

@ -0,0 +1,22 @@
import * as React from 'react'
function ChevronRight(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
width={16}
height={16}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M5.694 2.266l-.955.954 4.763 4.763-4.763 4.762.955.954 5.712-5.716-5.712-5.717z"
fill="#fff"
fillOpacity={0.6}
/>
</svg>
)
}
export default ChevronRight

View file

@ -0,0 +1,24 @@
import * as React from 'react'
function Cube(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
width={16}
height={16}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M7.51.953L2.01 4.046c-.314.178-.51.51-.511.87v6.168c.002.354.202.695.51.87l5.5 3.093c.298.171.682.171.98 0l5.5-3.093c.308-.175.508-.516.51-.87V4.919a1.008 1.008 0 00-.51-.872L8.49.953a1.003 1.003 0 00-.98 0zm5.474 3.674L8 1.824l-4.977 2.8 5.03 2.804 4.93-2.8zM2.5 5.477v5.605l5.007 2.816.047-5.604L2.5 5.477zm6.007 8.414l4.99-2.807.003-5.6-4.946 2.81-.047 5.597z"
fill="#fff"
fillOpacity={0.6}
/>
</svg>
)
}
export default Cube

View file

@ -0,0 +1,22 @@
import * as React from 'react'
function CubeFull(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
width={16}
height={16}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M2.135 4.253l5.968 3.241 5.746-3.241-5.252-3.064a1 1 0 00-.993-.008L2.135 4.253zM7.586 14.947V8.338l-5.922-3.25v5.918a1 1 0 00.507.87l5.415 3.071zM8.414 14.947V8.338l5.922-3.25v5.918a1 1 0 01-.507.87l-5.415 3.071z"
fill="#fff"
fillOpacity={0.6}
/>
</svg>
)
}
export default CubeFull

View file

@ -0,0 +1,22 @@
import * as React from 'react'
function CubeHalf(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
width={16}
height={16}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M13.988 4.05L8.488.958a1.015 1.015 0 00-.975 0l-5.5 3.094c-.286.161-.514.533-.513.868v6.163c0 .356.202.7.513.875l5.5 3.094c.3.164.671.168.975 0l5.5-3.094c.31-.175.511-.519.512-.875V4.919c.002-.327-.223-.705-.512-.868zM8.056 7.427l-5.031-2.8L8 1.826l4.981 2.8-4.925 2.8zm5.444 3.656l-4.994 2.813.05-5.6L13.5 5.481v5.6z"
fill="#fff"
fillOpacity={0.6}
/>
</svg>
)
}
export default CubeHalf

View file

@ -0,0 +1,29 @@
import * as React from 'react'
function CubeRendered(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
width={16}
height={16}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M8.202 2.973l-2.92 1.58 2.714 1.58 2.817-1.58-2.61-1.58zM3.532 10.555v-3.22l3.031 1.72v3l-3.03-1.5zM12.532 10.555v-3.22l-3.031 1.72v3l3.031-1.5z"
fill="#fff"
fillOpacity={0.6}
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M7.51.955c.298-.171.682-.171.98 0l5.5 3.093c.315.179.508.51.51.87v6.165a1.024 1.024 0 01-.51.873l-5.5 3.093a1.003 1.003 0 01-.98 0L2.01 11.957a1.025 1.025 0 01-.511-.874V4.918c.002-.355.203-.696.51-.87L7.51.955zm.49.871l4.982 2.802-4.928 2.8-5.03-2.803L8 1.826zm-5.5 9.255V5.477l5.054 2.817-.047 5.606L2.5 11.08zm6.007 2.812l4.99-2.807.003-5.602-4.946 2.81-.047 5.599z"
fill="#fff"
fillOpacity={0.6}
/>
</svg>
)
}
export default CubeRendered

View file

@ -0,0 +1,23 @@
import * as React from 'react'
function Details(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
width={16}
height={16}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.5 3c-1.072 0-1.969.904-1.969 1.969 0 1 .929 1.968 1.969 1.968h9A1.969 1.969 0 1012.5 3h-9zm9 1H5.531v1.938H12.5A.969.969 0 0012.5 4zM3.5 9.14a1.969 1.969 0 000 3.938h9a1.969 1.969 0 100-3.937h-9zm9 1H8.406v1.938H12.5a.969.969 0 100-1.937z"
fill="#A7A8A9"
/>
</svg>
)
}
export default Details

View file

@ -0,0 +1,24 @@
import * as React from 'react'
function Ellipsis(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
width={16}
height={16}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M.166 7.994a2.26 2.26 0 114.518 0 2.26 2.26 0 01-4.518 0zM2.425 6.91a1.085 1.085 0 100 2.17 1.085 1.085 0 000-2.17zM5.74 7.994a2.26 2.26 0 114.519 0 2.26 2.26 0 01-4.519 0zM8 6.91a1.085 1.085 0 100 2.17 1.085 1.085 0 000-2.17zM13.575 5.735a2.26 2.26 0 100 4.519 2.26 2.26 0 000-4.52zm-1.086 2.26a1.085 1.085 0 112.171 0 1.085 1.085 0 01-2.17 0z"
fill="#fff"
fillOpacity={0.6}
/>
</svg>
)
}
export default Ellipsis

View file

@ -0,0 +1,24 @@
import * as React from 'react'
function GlobeSimple(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
width={16}
height={16}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1.5 8a6.5 6.5 0 1113 0 6.5 6.5 0 01-13 0zm4.75-5.216A5.505 5.505 0 002.522 7.5H5.01c.051-1.468.328-2.807.764-3.825.14-.326.298-.626.477-.89zM13.478 7.5A5.505 5.505 0 009.75 2.784c.179.265.338.565.477.891.436 1.018.713 2.357.764 3.825h2.487zm0 1a5.505 5.505 0 01-3.729 4.716c.18-.264.339-.566.478-.892.436-1.018.713-2.356.764-3.824h2.487zM6.25 13.216A5.505 5.505 0 012.522 8.5H5.01c.051 1.468.328 2.806.764 3.824.14.326.299.627.478.892zm.44-9.147c-.374.874-.63 2.074-.682 3.431h3.982c-.051-1.357-.308-2.557-.683-3.431-.21-.491-.448-.857-.686-1.092-.236-.234-.446-.315-.622-.315s-.386.081-.622.315c-.238.235-.476.6-.686 1.092zm2.617 7.862c.375-.875.631-2.075.683-3.431H6.009c.052 1.356.308 2.556.683 3.43.21.492.448.857.686 1.093.236.233.446.314.622.314s.386-.081.622-.314c.238-.236.476-.601.686-1.092z"
fill="#fff"
fillOpacity={0.6}
/>
</svg>
)
}
export default GlobeSimple

View file

@ -0,0 +1,23 @@
import * as React from 'react'
function Outline(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
width={16}
height={16}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1.775 2.781a.5.5 0 01.5.5v1.7H4.67c.108-.957.92-1.7 1.905-1.7h6.608a1.917 1.917 0 110 3.834H6.574c-.78 0-1.452-.466-1.751-1.135H2.275v5.03h2.39a2.032 2.032 0 012.023-1.854h6.38a2.031 2.031 0 110 4.063h-6.38c-.83 0-1.543-.497-1.858-1.21H1.775a.5.5 0 01-.5-.5V3.281a.5.5 0 01.5-.5zm4.799 1.5h6.608a.917.917 0 110 1.834H6.574a.917.917 0 110-1.834zm.114 5.875h6.38a1.031 1.031 0 110 2.063h-6.38a1.032 1.032 0 110-2.063z"
fill="#A8A8A9"
/>
</svg>
)
}
export default Outline

View file

@ -0,0 +1,22 @@
import * as React from 'react'
function Package(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
width={16}
height={16}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M8.339 4.5l-2.055.644 4.451 1.393v2.748l-2.966.928-2.504-.783V6.738l2.42.758 2.055-.644-4.458-1.395L4 5.858v4.463L7.768 11.5 12 10.175V5.646L8.339 4.5z"
fill="#fff"
fillOpacity={0.6}
/>
</svg>
)
}
export default Package

View file

@ -0,0 +1,29 @@
import * as React from 'react'
function Resize(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
width={16}
height={16}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1.452 3.452a2 2 0 012-2h9.096a2 2 0 012 2v9.096a2 2 0 01-2 2H3.452a2 2 0 01-2-2V3.452zm2-1h9.096a1 1 0 011 1v9.096a1 1 0 01-1 1h-5.06V8.511H2.451V3.452a1 1 0 011-1z"
fill="#fff"
fillOpacity={0.6}
/>
<path
d="M12.501 4.09a.5.5 0 00-.5-.5H8.95a.5.5 0 100 1h1.98l-2.45 2.449a.5.5 0 10.707.707l2.315-2.315v1.627a.5.5 0 001 0V4.09z"
fill="#fff"
fillOpacity={0.6}
/>
</svg>
)
}
export default Resize

View file

@ -0,0 +1,16 @@
export {default as Outline} from './Outline'
export {default as ArrowClockwise} from './ArrowClockwise'
export {default as ArrowsOutCardinal} from './ArrowsOutCardinal'
export {default as Camera} from './Camera'
export {default as ChevronDown} from './ChevronDown'
export {default as ChevronRight} from './ChevronRight'
export {default as ChevronLeft} from './ChevronLeft'
export {default as Cube} from './Cube'
export {default as CubeFull} from './CubeFull'
export {default as CubeHalf} from './CubeHalf'
export {default as CubeRendered} from './CubeRendered'
export {default as Details} from './Details'
export {default as Ellipsis} from './Ellipsis'
export {default as GlobeSimple} from './GlobeSimple'
export {default as Resize} from './Resize'
export {default as Package} from './Package'

View file

@ -5,6 +5,7 @@ import type {$FixMe, $IntentionalAny} from '@theatre/shared/utils/types'
import useTooltip from '@theatre/studio/uiComponents/Popover/useTooltip' import useTooltip from '@theatre/studio/uiComponents/Popover/useTooltip'
import mergeRefs from 'react-merge-refs' import mergeRefs from 'react-merge-refs'
import MinimalTooltip from '@theatre/studio/uiComponents/Popover/MinimalTooltip' import MinimalTooltip from '@theatre/studio/uiComponents/Popover/MinimalTooltip'
import ToolbarSwitchSelectContainer from './ToolbarSwitchSelectContainer'
const Container = styled.button` const Container = styled.button`
${pointerEventsAutoInNormalMode}; ${pointerEventsAutoInNormalMode};
@ -14,45 +15,65 @@ const Container = styled.button`
justify-content: center; justify-content: center;
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
width: 24px; width: 32px;
height: 24px; height: 32px;
outline: none; outline: none;
color: rgba(255, 255, 255, 0.75); color: #a8a8a9;
background-color: rgb(47, 49, 53);
background: rgba(40, 43, 47, 0.45);
backdrop-filter: blur(14px);
border: none;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
filter: drop-shadow(0px 1px 1px rgba(0, 0, 0, 0.25))
drop-shadow(0px 2px 6px rgba(0, 0, 0, 0.15));
&:hover { &:hover {
color: #fff; background: rgba(59, 63, 69, 0.8);
}
background-color: rgba(28, 30, 32, 0.95); &:active {
&:after { background: rgba(82, 88, 96, 0.8);
border-color: rgba(90, 90, 90, 1);
}
} }
&.selected { &.selected {
color: #fff; background: rgba(40, 43, 47, 0.9);
color: white;
}
background-color: rgba(17, 18, 20, 0.95); &:first-child {
&:after { border-top-left-radius: 2px;
border-color: rgb(43, 43, 43); border-bottom-left-radius: 2px;
}
&:last-child {
border-bottom-right-radius: 2px;
border-top-right-radius: 2px;
}
// Don't blur if in a button group, because it already blurs. We need to blur
// on the group-level, otherwise we get seams.
${ToolbarSwitchSelectContainer} > & {
backdrop-filter: none;
filter: none;
}
@supports not (backdrop-filter: blur()) {
background: rgba(40, 43, 47, 0.8);
&:hover {
background: rgba(59, 63, 69, 0.8);
}
&:active {
background: rgba(82, 88, 96, 0.7);
}
&.selected {
background: rgb(27, 32, 35);
} }
} }
&:before {
border: 1px solid rgb(62, 62, 62);
position: absolute;
inset: -1px;
display: block;
content: ' ';
z-index: -1;
pointer-events: none;
border-radius: 2px;
box-sizing: border-box;
box-shadow: 0px 3px 4px -3px rgba(0, 0, 0, 0.49);
}
border: 0;
` `
const ToolbarIconButton: typeof Container = React.forwardRef( const ToolbarIconButton: typeof Container = React.forwardRef(

View file

@ -1,11 +1,9 @@
import type {ReactElement} from 'react' import type {ReactElement} from 'react'
import React from 'react' import React from 'react'
import type {IconType} from 'react-icons' import type {IconType} from 'react-icons'
import {Group, Button} from 'reakit' import {Button} from 'reakit'
import styled from 'styled-components'
import ButtonImpl from './ToolbarIconButton' import ButtonImpl from './ToolbarIconButton'
import Container from './ToolbarSwitchSelectContainer'
const Opt = styled(ButtonImpl)``
function OptionButton<T>({ function OptionButton<T>({
value, value,
@ -22,7 +20,7 @@ function OptionButton<T>({
}) { }) {
return ( return (
<> <>
<Opt <ButtonImpl
forwardedAs={Button} forwardedAs={Button}
className={isSelected ? 'selected' : undefined} className={isSelected ? 'selected' : undefined}
aria-label={label} aria-label={label}
@ -30,7 +28,7 @@ function OptionButton<T>({
title={label} title={label}
> >
{icon} {icon}
</Opt> </ButtonImpl>
</> </>
) )
} }
@ -45,11 +43,6 @@ interface Props<Option> {
}[] }[]
} }
const Container = styled(Group)`
display: flex;
gap: 5px;
`
const ToolbarSwitchSelect = <Option extends string | number>({ const ToolbarSwitchSelect = <Option extends string | number>({
value: valueOfSwitch, value: valueOfSwitch,
onChange, onChange,

View file

@ -0,0 +1,13 @@
import styled from 'styled-components'
import {Group} from 'reakit'
const Container = styled(Group)`
display: flex;
height: fit-content;
backdrop-filter: blur(14px);
border-radius: 2px;
filter: drop-shadow(0px 1px 1px rgba(0, 0, 0, 0.25))
drop-shadow(0px 2px 6px rgba(0, 0, 0, 0.15));
`
export default Container

View file

@ -0,0 +1,34 @@
import {useEffect, useState} from 'react'
export default function useHotspot(spot: 'left' | 'right') {
const [active, setActive] = useState(false)
useEffect(() => {
const hoverListener = (e: MouseEvent) => {
const threshold = active ? 200 : 50
const mouseInside =
spot === 'left' ? e.x < threshold : e.x > window.innerWidth - threshold
if (mouseInside) {
setActive(true)
} else {
setActive(false)
}
}
document.addEventListener('mousemove', hoverListener)
const leaveListener = () => {
setActive(false)
}
document.addEventListener('mouseleave', leaveListener)
return () => {
document.removeEventListener('mousemove', hoverListener)
document.removeEventListener('mouseleave', leaveListener)
}
}, [active])
return active
}