UI improvements/sidebar pinning (#175)
Co-authored-by: Aria Minaei <aria.minaei@gmail.com>
This commit is contained in:
parent
ec18687a98
commit
0690a85ae2
46 changed files with 998 additions and 501 deletions
|
@ -12,7 +12,12 @@ test.describe('setting-static-props', () => {
|
|||
test('Undo/redo', async ({page}) => {
|
||||
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"]')
|
||||
|
||||
|
|
|
@ -99,8 +99,6 @@ const ReferenceWindow: VFC<ReferenceWindowProps> = ({height}) => {
|
|||
|
||||
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
|
||||
ctx.imageSmoothingQuality = 'high'
|
||||
|
||||
|
|
|
@ -15,7 +15,6 @@ import {getEditorSheet, getEditorSheetObject} from './editorStuff'
|
|||
import type {$IntentionalAny} from '@theatre/shared/utils/types'
|
||||
import {InfiniteGridHelper} from '../InfiniteGridHelper'
|
||||
import {DragDetectorProvider} from './DragDetector'
|
||||
import TooltipPortalProvider from './TooltipPortalProvider'
|
||||
import ReferenceWindow from './ReferenceWindow/ReferenceWindow'
|
||||
|
||||
const GlobalStyle = createGlobalStyle`
|
||||
|
@ -182,39 +181,37 @@ const SnapshotEditor: React.FC<{paneId: string}> = (props) => {
|
|||
<StyleSheetManager disableVendorPrefixes>
|
||||
<>
|
||||
<GlobalStyle />
|
||||
<TooltipPortalProvider>
|
||||
<Wrapper>
|
||||
<Overlay>
|
||||
<Tools ref={setToolsContainer} />
|
||||
{showReferenceWindow && (
|
||||
<ReferenceWindowContainer>
|
||||
<ReferenceWindow height={120} />
|
||||
</ReferenceWindowContainer>
|
||||
)}
|
||||
</Overlay>
|
||||
<Wrapper>
|
||||
<Overlay>
|
||||
<Tools ref={setToolsContainer} />
|
||||
{showReferenceWindow && (
|
||||
<ReferenceWindowContainer>
|
||||
<ReferenceWindow height={120} />
|
||||
</ReferenceWindowContainer>
|
||||
)}
|
||||
</Overlay>
|
||||
|
||||
{sceneSnapshot ? (
|
||||
<>
|
||||
<CanvasWrapper>
|
||||
<Canvas
|
||||
onCreated={({gl}) => {
|
||||
gl.setClearColor('white')
|
||||
}}
|
||||
shadows
|
||||
dpr={[1, 2]}
|
||||
frameloop="demand"
|
||||
onPointerMissed={onPointerMissed}
|
||||
>
|
||||
<EditorScene
|
||||
snapshotEditorSheet={snapshotEditorSheet}
|
||||
paneId={paneId}
|
||||
/>
|
||||
</Canvas>
|
||||
</CanvasWrapper>
|
||||
</>
|
||||
) : null}
|
||||
</Wrapper>
|
||||
</TooltipPortalProvider>
|
||||
{sceneSnapshot ? (
|
||||
<>
|
||||
<CanvasWrapper>
|
||||
<Canvas
|
||||
onCreated={({gl}) => {
|
||||
gl.setClearColor('white')
|
||||
}}
|
||||
shadows
|
||||
dpr={[1, 2]}
|
||||
frameloop="demand"
|
||||
onPointerMissed={onPointerMissed}
|
||||
>
|
||||
<EditorScene
|
||||
snapshotEditorSheet={snapshotEditorSheet}
|
||||
paneId={paneId}
|
||||
/>
|
||||
</Canvas>
|
||||
</CanvasWrapper>
|
||||
</>
|
||||
) : null}
|
||||
</Wrapper>
|
||||
</>
|
||||
</StyleSheetManager>
|
||||
</root.div>
|
||||
|
|
|
@ -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
|
|
@ -21,6 +21,27 @@ const r3fExtension: IExtension = {
|
|||
id: '@theatre/r3f',
|
||||
toolbars: {
|
||||
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 editorObject = getEditorSheetObject()
|
||||
|
||||
|
@ -34,11 +55,9 @@ const r3fExtension: IExtension = {
|
|||
return [
|
||||
{
|
||||
type: 'Icon',
|
||||
title: 'Create Snapshot',
|
||||
svgSource: io5CameraOutline,
|
||||
onClick: () => {
|
||||
studio.createPane('snapshot')
|
||||
},
|
||||
onClick: createSnapshot,
|
||||
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>`,
|
||||
},
|
||||
{
|
||||
type: 'Switch',
|
||||
|
@ -121,19 +140,6 @@ const r3fExtension: IExtension = {
|
|||
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: [
|
||||
{
|
||||
|
|
|
@ -5,8 +5,8 @@ import ReactDOM from 'react-dom'
|
|||
import type {Studio} from './Studio'
|
||||
import {val} from '@theatre/dataverse'
|
||||
import {getMounter} from './utils/renderInPortalInContext'
|
||||
import {Toolbar} from './toolbars/GlobalToolbar/GlobalToolbar'
|
||||
import {withStyledShadow} from './css'
|
||||
import ExtensionToolbar from './toolbars/ExtensionToolbar/ExtensionToolbar'
|
||||
|
||||
export default class UI {
|
||||
readonly containerEl = document.createElement('div')
|
||||
|
@ -93,7 +93,11 @@ export default class UI {
|
|||
renderToolset(toolsetId: string, htmlNode: HTMLElement) {
|
||||
const s = getMounter()
|
||||
|
||||
s.mountOrRender(withStyledShadow(Toolbar), {toolbarId: toolsetId}, htmlNode)
|
||||
s.mountOrRender(
|
||||
withStyledShadow(ExtensionToolbar),
|
||||
{toolbarId: toolsetId},
|
||||
htmlNode,
|
||||
)
|
||||
|
||||
return s.unmount
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import {val} from '@theatre/dataverse'
|
|||
import React, {useEffect} from 'react'
|
||||
import styled, {createGlobalStyle} from 'styled-components'
|
||||
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 {PortalContext} from 'reakit'
|
||||
import type {$IntentionalAny} from '@theatre/shared/utils/types'
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {getOutlineSelection} from '@theatre/studio/selectors'
|
||||
import {usePrism} from '@theatre/react'
|
||||
import React from 'react'
|
||||
import {usePrism, useVal} from '@theatre/react'
|
||||
import React, {useEffect, useLayoutEffect} from 'react'
|
||||
import styled from 'styled-components'
|
||||
import {isProject, isSheetObject} from '@theatre/shared/instanceTypes'
|
||||
import {
|
||||
|
@ -11,46 +11,35 @@ import {
|
|||
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||
import ObjectDetails from './ObjectDetails'
|
||||
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`
|
||||
background-color: transparent;
|
||||
pointer-events: none;
|
||||
const headerHeight = `32px`
|
||||
|
||||
const Container = styled.div<{pin: boolean}>`
|
||||
background-color: rgba(40, 43, 47, 0.8);
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 12px;
|
||||
bottom: 0px;
|
||||
right: 8px;
|
||||
top: 50px;
|
||||
width: 236px;
|
||||
height: fit-content;
|
||||
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;
|
||||
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`
|
||||
margin: 0 10px;
|
||||
color: #ffffffc2;
|
||||
color: #919191;
|
||||
font-weight: 500;
|
||||
font-size: 10px;
|
||||
user-select: none;
|
||||
|
@ -60,37 +49,15 @@ const Title = styled.div`
|
|||
text-overflow: ellipsis;
|
||||
`
|
||||
|
||||
const headerHeight = `32px`
|
||||
|
||||
const Header = styled.div`
|
||||
height: ${headerHeight};
|
||||
display: flex;
|
||||
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`
|
||||
${pointerEventsAutoInNormalMode};
|
||||
position: absolute;
|
||||
top: ${headerHeight};
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: auto;
|
||||
max-height: calc(100% - ${headerHeight});
|
||||
max-height: calc(100vh - 100px);
|
||||
overflow-y: scroll;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
|
@ -102,56 +69,87 @@ const Body = styled.div`
|
|||
`
|
||||
|
||||
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(() => {
|
||||
const selection = getOutlineSelection()
|
||||
|
||||
const obj = selection.find(isSheetObject)
|
||||
if (obj) {
|
||||
return (
|
||||
<Container>
|
||||
<Content data-testid="DetailPanel-Object">
|
||||
<Header>
|
||||
<Title
|
||||
title={`${obj.sheet.address.sheetId}: ${obj.sheet.address.sheetInstanceId} > ${obj.address.objectKey}`}
|
||||
>
|
||||
<TitleBar_Piece>{obj.sheet.address.sheetId} </TitleBar_Piece>
|
||||
<Container
|
||||
data-testid="DetailPanel-Object"
|
||||
pin={pin || hostspotActive}
|
||||
onMouseEnter={() => {
|
||||
isDetailPanelHoveredB.set(true)
|
||||
}}
|
||||
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>{':'} </TitleBar_Punctuation>
|
||||
<TitleBar_Piece>
|
||||
{obj.sheet.address.sheetInstanceId}{' '}
|
||||
</TitleBar_Piece>
|
||||
<TitleBar_Punctuation>{':'} </TitleBar_Punctuation>
|
||||
<TitleBar_Piece>
|
||||
{obj.sheet.address.sheetInstanceId}{' '}
|
||||
</TitleBar_Piece>
|
||||
|
||||
<TitleBar_Punctuation> {'>'} </TitleBar_Punctuation>
|
||||
<TitleBar_Piece>{obj.address.objectKey}</TitleBar_Piece>
|
||||
</Title>
|
||||
</Header>
|
||||
<Body>
|
||||
<ObjectDetails objects={[obj]} />
|
||||
</Body>
|
||||
</Content>
|
||||
<TitleBar_Punctuation> → </TitleBar_Punctuation>
|
||||
<TitleBar_Piece>{obj.address.objectKey}</TitleBar_Piece>
|
||||
</Title>
|
||||
</Header>
|
||||
<Body>
|
||||
<ObjectDetails objects={[obj]} />
|
||||
</Body>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
const project = selection.find(isProject)
|
||||
if (project) {
|
||||
return (
|
||||
<Container>
|
||||
<Content>
|
||||
<Header>
|
||||
<Title title={`${project.address.projectId}`}>
|
||||
<TitleBar_Piece>{project.address.projectId} </TitleBar_Piece>
|
||||
</Title>
|
||||
</Header>
|
||||
<Body>
|
||||
<ProjectDetails projects={[project]} />
|
||||
</Body>
|
||||
</Content>
|
||||
<Container pin={pin || hostspotActive}>
|
||||
<Header>
|
||||
<Title title={`${project.address.projectId}`}>
|
||||
<TitleBar_Piece>{project.address.projectId} </TitleBar_Piece>
|
||||
</Title>
|
||||
</Header>
|
||||
<Body>
|
||||
<ProjectDetails projects={[project]} />
|
||||
</Body>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
return <></>
|
||||
}, [])
|
||||
}, [pin, hostspotActive])
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
|
|
|
@ -6,10 +6,7 @@ import last from 'lodash-es/last'
|
|||
import {darken, transparentize} from 'polished'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import {
|
||||
indentationFormula,
|
||||
rowBg,
|
||||
} from '@theatre/studio/panels/DetailPanel/DeterminePropEditorForDetail/SingleRowPropEditor'
|
||||
import {indentationFormula} from '@theatre/studio/panels/DetailPanel/DeterminePropEditorForDetail/SingleRowPropEditor'
|
||||
import {propNameTextCSS} from '@theatre/studio/propEditors/utils/propNameTextCSS'
|
||||
import DefaultOrStaticValueIndicator from '@theatre/studio/propEditors/DefaultValueIndicator'
|
||||
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||
|
@ -29,8 +26,6 @@ const Header = styled.div`
|
|||
display: flex;
|
||||
align-items: stretch;
|
||||
position: relative;
|
||||
|
||||
${rowBg};
|
||||
`
|
||||
|
||||
const Padding = styled.div`
|
||||
|
|
|
@ -6,39 +6,12 @@ import useRefAndState from '@theatre/studio/utils/useRefAndState'
|
|||
import {last} from 'lodash-es'
|
||||
import React from 'react'
|
||||
import type {useEditingToolsForSimplePropInDetailsPanel} from '@theatre/studio/propEditors/useEditingToolsForSimpleProp'
|
||||
import styled, {css} from 'styled-components'
|
||||
import {transparentize} from 'polished'
|
||||
import styled from 'styled-components'
|
||||
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||
import {propNameTextCSS} from '@theatre/studio/propEditors/utils/propNameTextCSS'
|
||||
|
||||
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`
|
||||
display: flex;
|
||||
height: 30px;
|
||||
|
@ -47,8 +20,6 @@ const LeftRow = styled.div`
|
|||
--right-width: 60%;
|
||||
position: relative;
|
||||
${pointerEventsAutoInNormalMode};
|
||||
|
||||
${rowBg};
|
||||
`
|
||||
|
||||
const Left = styled.div`
|
||||
|
|
|
@ -5,12 +5,9 @@ import usePopover from '@theatre/studio/uiComponents/Popover/usePopover'
|
|||
import React, {useCallback, useState} from 'react'
|
||||
import styled from 'styled-components'
|
||||
import DetailPanelButton from '@theatre/studio/uiComponents/DetailPanelButton'
|
||||
import {rowBgColor} from './DeterminePropEditorForDetail/SingleRowPropEditor'
|
||||
import StateConflictRow from './ProjectDetails/StateConflictRow'
|
||||
|
||||
const Container = styled.div`
|
||||
background-color: ${rowBgColor};
|
||||
`
|
||||
const Container = styled.div``
|
||||
|
||||
const TheExportRow = styled.div`
|
||||
padding: 8px 10px;
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import type {VoidFn} from '@theatre/shared/utils/types'
|
||||
import React from 'react'
|
||||
import {GoChevronRight, DiHtml53DEffects} from 'react-icons/all'
|
||||
import styled, {css} from 'styled-components'
|
||||
import noop from '@theatre/shared/utils/noop'
|
||||
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||
import {ChevronRight, Package} from '@theatre/studio/uiComponents/icons'
|
||||
|
||||
export const Container = styled.li`
|
||||
margin: 0;
|
||||
|
@ -17,14 +17,16 @@ export const Container = styled.li`
|
|||
|
||||
export const BaseHeader = styled.div``
|
||||
|
||||
const baseBg = `#3e4447`
|
||||
|
||||
const baseBorderColor = `#34343e`
|
||||
|
||||
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;
|
||||
height: 28px;
|
||||
gap: 8px;
|
||||
height: 23px;
|
||||
line-height: 0;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
|
@ -32,29 +34,42 @@ const Header = styled(BaseHeader)`
|
|||
pointer-events: none;
|
||||
white-space: nowrap;
|
||||
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
--item-bg: ${baseBg};
|
||||
--item-border-color: ${baseBorderColor};
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 3px 4px -1px rgba(0, 0, 0, 0.48);
|
||||
|
||||
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 {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
|
||||
--item-bg: #2e4244ed;
|
||||
--item-border-color: #254355;
|
||||
background: rgba(29, 53, 59, 0.7);
|
||||
}
|
||||
|
||||
&:not(.not-selectable):hover {
|
||||
color: #fff;
|
||||
&:not(.not-selectable):not(.selected):hover {
|
||||
background: rgba(59, 63, 69, 0.9);
|
||||
|
||||
--item-bg: #1e5866;
|
||||
--item-border-color: #152f42;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.24);
|
||||
}
|
||||
|
||||
&:not(.not-selectable):not(.selected):active {
|
||||
background: rgba(82, 88, 96, 0.9);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.24);
|
||||
}
|
||||
|
||||
&.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;
|
||||
--item-border-color: #152f42;
|
||||
// Hit zone
|
||||
&: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`
|
||||
${outlineItemFont};
|
||||
|
||||
padding: 2px 8px;
|
||||
${pointerEventsAutoInNormalMode};
|
||||
position: relative;
|
||||
// Compensate for border bottom
|
||||
top: 1px;
|
||||
display: flex;
|
||||
height: 17px;
|
||||
height: 20px;
|
||||
align-items: center;
|
||||
|
||||
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};
|
||||
}
|
||||
box-sizing: border-box;
|
||||
`
|
||||
|
||||
const Head_IconContainer = styled.span`
|
||||
width: 18px;
|
||||
box-sizing: border-box;
|
||||
height: 18px;
|
||||
margin-right: 4px;
|
||||
const Head_IconContainer = styled.div`
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
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}>`
|
||||
|
@ -160,14 +137,14 @@ const BaseItem: React.FC<{
|
|||
{'--depth': depth}
|
||||
}
|
||||
>
|
||||
<Header className={selectionStatus} onClick={select ?? noop}>
|
||||
<Header className={selectionStatus} onClick={select ?? noop} data-header>
|
||||
<Head_IconContainer>
|
||||
{canContainChildren ? (
|
||||
<Head_Icon_WithDescendants isOpen={true}>
|
||||
<GoChevronRight />
|
||||
<ChevronRight />
|
||||
</Head_Icon_WithDescendants>
|
||||
) : (
|
||||
<DiHtml53DEffects />
|
||||
<Package />
|
||||
)}
|
||||
</Head_IconContainer>
|
||||
|
||||
|
|
|
@ -1,188 +1,71 @@
|
|||
import React from 'react'
|
||||
import React, {useEffect, useState} from 'react'
|
||||
import styled from 'styled-components'
|
||||
import {panelZIndexes} from '@theatre/studio/panels/BasePanel/common'
|
||||
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/react'
|
||||
import {useVal} from '@theatre/react'
|
||||
import getStudio from '@theatre/studio/getStudio'
|
||||
import {val} from '@theatre/dataverse'
|
||||
import useTooltip from '@theatre/studio/uiComponents/Popover/useTooltip'
|
||||
import BasicTooltip from '@theatre/studio/uiComponents/Popover/BasicTooltip'
|
||||
import ErrorTooltip from '@theatre/studio/uiComponents/Popover/ErrorTooltip'
|
||||
import useHotspot from '@theatre/studio/uiComponents/useHotspot'
|
||||
|
||||
const Container = styled.div`
|
||||
const headerHeight = `44px`
|
||||
|
||||
const Container = styled.div<{pin: boolean}>`
|
||||
background-color: transparent;
|
||||
pointer-events: none;
|
||||
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;
|
||||
height: auto;
|
||||
z-index: ${panelZIndexes.outlinePanel};
|
||||
${pointerEventsAutoInNormalMode};
|
||||
top: calc(${headerHeight} + 8px);
|
||||
height: fit-content;
|
||||
max-height: calc(100% - ${headerHeight});
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
padding: 0;
|
||||
user-select: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
scrollbar-width: 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;
|
||||
display: ${({pin}) => (pin ? 'block' : 'none')};
|
||||
|
||||
&:hover {
|
||||
display: block;
|
||||
}
|
||||
|
||||
// Create a small buffer on the bottom to aid selecting the bottom item in a long, scrolling list
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
height: 20px;
|
||||
}
|
||||
`
|
||||
|
||||
const OutlinePanel: React.FC<{}> = (props) => {
|
||||
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 pin = useVal(getStudio().atomP.ahistoric.pinOutline) !== false
|
||||
const show = useVal(getStudio().atomP.ephemeral.showOutline)
|
||||
const active = useHotspot('left')
|
||||
const [hovered, setHovered] = useState(false)
|
||||
|
||||
const [triggerTooltip, triggerButtonRef] = useTooltip<HTMLButtonElement>(
|
||||
{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>
|
||||
),
|
||||
)
|
||||
useEffect(() => {
|
||||
getStudio().transaction(({stateEditors, drafts}) => {
|
||||
stateEditors.studio.ephemeral.setShowOutline(active || hovered)
|
||||
})
|
||||
}, [active, hovered])
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<TriggerContainer>
|
||||
{triggerTooltip}
|
||||
<TriggerButton
|
||||
ref={triggerButtonRef}
|
||||
data-testid="OutlinePanel-TriggerButton"
|
||||
>
|
||||
<VscListTree />
|
||||
</TriggerButton>
|
||||
{conflicts.length > 0 ? (
|
||||
<NumberOfConflictsIndicator>
|
||||
{conflicts.length}
|
||||
</NumberOfConflictsIndicator>
|
||||
) : null}
|
||||
{/* <Title>Outline</Title> */}
|
||||
</TriggerContainer>
|
||||
<Content>
|
||||
<Body data-testid="OutlinePanel-Content">
|
||||
<ProjectsList />
|
||||
</Body>
|
||||
</Content>
|
||||
<Container
|
||||
pin={pin || show}
|
||||
onMouseEnter={() => {
|
||||
setHovered(true)
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHovered(false)
|
||||
}}
|
||||
>
|
||||
<ProjectsList />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ const ColorPreviewPuck = styled.div.attrs<ColorPreviewPuckProps>((props) => ({
|
|||
}))<ColorPreviewPuckProps>`
|
||||
height: 18px;
|
||||
aspect-ratio: 1;
|
||||
border-radius: 2px;
|
||||
border-radius: 99999px;
|
||||
`
|
||||
|
||||
const HexInput = styled(BasicStringInput)`
|
||||
|
@ -46,14 +46,13 @@ const RgbaPopover = styled.div`
|
|||
position: absolute;
|
||||
background-color: ${popoverBackgroundColor};
|
||||
color: white;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
cursor: default;
|
||||
border-radius: 3px;
|
||||
z-index: 10000;
|
||||
backdrop-filter: blur(8px);
|
||||
|
||||
padding: 4;
|
||||
padding: 4px;
|
||||
pointer-events: all;
|
||||
|
||||
border: none;
|
||||
|
|
|
@ -3,6 +3,6 @@ import {css} from 'styled-components'
|
|||
export const propNameTextCSS = css`
|
||||
font-weight: 300;
|
||||
font-size: 11px;
|
||||
color: #9a9a9a;
|
||||
color: #919191;
|
||||
text-shadow: 0.5px 0.5px 2px rgba(0, 0, 0, 0.3);
|
||||
`
|
||||
|
|
|
@ -41,6 +41,7 @@ const initialState: StudioState = {
|
|||
byId: {},
|
||||
paneClasses: {},
|
||||
},
|
||||
showOutline: false,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -42,6 +42,7 @@ import type {
|
|||
OutlineSelectionState,
|
||||
PanelPosition,
|
||||
StudioAhistoricState,
|
||||
StudioEphemeralState,
|
||||
StudioHistoricStateSequenceEditorMarker,
|
||||
} from './types'
|
||||
import {clamp, uniq} from 'lodash-es'
|
||||
|
@ -339,6 +340,11 @@ namespace stateEditors {
|
|||
}
|
||||
}
|
||||
export namespace ephemeral {
|
||||
export function setShowOutline(
|
||||
showOutline: StudioEphemeralState['showOutline'],
|
||||
) {
|
||||
drafts().ephemeral.showOutline = showOutline
|
||||
}
|
||||
export namespace projects {
|
||||
export namespace stateByProjectId {
|
||||
export function _ensure(p: ProjectAddress) {
|
||||
|
@ -400,6 +406,16 @@ namespace stateEditors {
|
|||
}
|
||||
}
|
||||
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(
|
||||
visibilityState: StudioAhistoricState['visibilityState'],
|
||||
) {
|
||||
|
|
|
@ -6,6 +6,14 @@ import type {PointableSet} from '@theatre/shared/utils/PointableSet'
|
|||
import type {StudioSheetItemKey} from '@theatre/shared/utils/ids'
|
||||
|
||||
export type StudioAhistoricState = {
|
||||
/**
|
||||
* undefined means the outline menu is pinned
|
||||
*/
|
||||
pinOutline?: boolean
|
||||
/**
|
||||
* undefined means the detail panel is pinned
|
||||
*/
|
||||
pinDetails?: boolean
|
||||
visibilityState: 'everythingIsHidden' | 'everythingIsVisible'
|
||||
clipboard?: {
|
||||
keyframes?: Keyframe[]
|
||||
|
|
|
@ -5,6 +5,26 @@ import type {
|
|||
PaneClassDefinition,
|
||||
} 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 = {
|
||||
initialised: boolean
|
||||
coreByProject: {[projectId in string]: ProjectState['ephemeral']}
|
||||
|
@ -35,4 +55,5 @@ export type StudioEphemeralState = {
|
|||
}
|
||||
}
|
||||
}
|
||||
showOutline: boolean
|
||||
}
|
||||
|
|
|
@ -3,21 +3,15 @@ import {useVal} from '@theatre/react'
|
|||
import type {IExtension} from '@theatre/studio'
|
||||
import {pointerEventsAutoInNormalMode} from '@theatre/studio/css'
|
||||
import getStudio from '@theatre/studio/getStudio'
|
||||
import {panelZIndexes} from '@theatre/studio/panels/BasePanel/common'
|
||||
import type {ToolsetConfig} from '@theatre/studio/TheatreStudio'
|
||||
import React, {useLayoutEffect, useMemo} from 'react'
|
||||
|
||||
import styled from 'styled-components'
|
||||
import Toolset from './Toolset'
|
||||
|
||||
const Container = styled.div`
|
||||
position: fixed;
|
||||
z-index: ${panelZIndexes.toolbar};
|
||||
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
left: 12px;
|
||||
height: 36px;
|
||||
pointer-events: none;
|
||||
/* pointer-events: none; */
|
||||
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
|
@ -60,17 +54,9 @@ const ExtensionToolsetRender: React.FC<{
|
|||
return <Toolset config={config} />
|
||||
}
|
||||
|
||||
const GlobalToolbar: React.FC<{}> = (props) => {
|
||||
return (
|
||||
<Container>
|
||||
<Bg>
|
||||
<Toolbar toolbarId="global" />
|
||||
</Bg>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export const Toolbar: React.FC<{toolbarId: string}> = ({toolbarId}) => {
|
||||
export const ExtensionToolbar: React.FC<{toolbarId: string}> = ({
|
||||
toolbarId,
|
||||
}) => {
|
||||
const groups: Array<React.ReactNode> = []
|
||||
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
|
||||
return <>{groups}</>
|
||||
|
||||
return <Container>{groups}</Container>
|
||||
}
|
||||
|
||||
export default GlobalToolbar
|
||||
export default ExtensionToolbar
|
130
theatre/studio/src/toolbars/GlobalToolbar.tsx
Normal file
130
theatre/studio/src/toolbars/GlobalToolbar.tsx
Normal 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
|
69
theatre/studio/src/toolbars/PinButton.tsx
Normal file
69
theatre/studio/src/toolbars/PinButton.tsx
Normal 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
|
22
theatre/studio/src/uiComponents/icons/ArrowClockwise.tsx
Normal file
22
theatre/studio/src/uiComponents/icons/ArrowClockwise.tsx
Normal 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
|
22
theatre/studio/src/uiComponents/icons/ArrowsOutCardinal.tsx
Normal file
22
theatre/studio/src/uiComponents/icons/ArrowsOutCardinal.tsx
Normal 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
|
31
theatre/studio/src/uiComponents/icons/Camera.tsx
Normal file
31
theatre/studio/src/uiComponents/icons/Camera.tsx
Normal 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
|
24
theatre/studio/src/uiComponents/icons/ChevronDown.tsx
Normal file
24
theatre/studio/src/uiComponents/icons/ChevronDown.tsx
Normal 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
|
22
theatre/studio/src/uiComponents/icons/ChevronLeft.tsx
Normal file
22
theatre/studio/src/uiComponents/icons/ChevronLeft.tsx
Normal 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
|
22
theatre/studio/src/uiComponents/icons/ChevronRight.tsx
Normal file
22
theatre/studio/src/uiComponents/icons/ChevronRight.tsx
Normal 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
|
24
theatre/studio/src/uiComponents/icons/Cube.tsx
Normal file
24
theatre/studio/src/uiComponents/icons/Cube.tsx
Normal 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
|
22
theatre/studio/src/uiComponents/icons/CubeFull.tsx
Normal file
22
theatre/studio/src/uiComponents/icons/CubeFull.tsx
Normal 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
|
22
theatre/studio/src/uiComponents/icons/CubeHalf.tsx
Normal file
22
theatre/studio/src/uiComponents/icons/CubeHalf.tsx
Normal 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
|
29
theatre/studio/src/uiComponents/icons/CubeRendered.tsx
Normal file
29
theatre/studio/src/uiComponents/icons/CubeRendered.tsx
Normal 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
|
23
theatre/studio/src/uiComponents/icons/Details.tsx
Normal file
23
theatre/studio/src/uiComponents/icons/Details.tsx
Normal 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
|
24
theatre/studio/src/uiComponents/icons/Ellipsis.tsx
Normal file
24
theatre/studio/src/uiComponents/icons/Ellipsis.tsx
Normal 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
|
24
theatre/studio/src/uiComponents/icons/GlobeSimple.tsx
Normal file
24
theatre/studio/src/uiComponents/icons/GlobeSimple.tsx
Normal 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
|
23
theatre/studio/src/uiComponents/icons/Outline.tsx
Normal file
23
theatre/studio/src/uiComponents/icons/Outline.tsx
Normal 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
|
22
theatre/studio/src/uiComponents/icons/Package.tsx
Normal file
22
theatre/studio/src/uiComponents/icons/Package.tsx
Normal 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
|
29
theatre/studio/src/uiComponents/icons/Resize.tsx
Normal file
29
theatre/studio/src/uiComponents/icons/Resize.tsx
Normal 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
|
16
theatre/studio/src/uiComponents/icons/index.ts
Normal file
16
theatre/studio/src/uiComponents/icons/index.ts
Normal 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'
|
|
@ -5,6 +5,7 @@ import type {$FixMe, $IntentionalAny} from '@theatre/shared/utils/types'
|
|||
import useTooltip from '@theatre/studio/uiComponents/Popover/useTooltip'
|
||||
import mergeRefs from 'react-merge-refs'
|
||||
import MinimalTooltip from '@theatre/studio/uiComponents/Popover/MinimalTooltip'
|
||||
import ToolbarSwitchSelectContainer from './ToolbarSwitchSelectContainer'
|
||||
|
||||
const Container = styled.button`
|
||||
${pointerEventsAutoInNormalMode};
|
||||
|
@ -14,45 +15,65 @@ const Container = styled.button`
|
|||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
outline: none;
|
||||
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
background-color: rgb(47, 49, 53);
|
||||
color: #a8a8a9;
|
||||
|
||||
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 {
|
||||
color: #fff;
|
||||
background: rgba(59, 63, 69, 0.8);
|
||||
}
|
||||
|
||||
background-color: rgba(28, 30, 32, 0.95);
|
||||
&:after {
|
||||
border-color: rgba(90, 90, 90, 1);
|
||||
}
|
||||
&:active {
|
||||
background: rgba(82, 88, 96, 0.8);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
color: #fff;
|
||||
background: rgba(40, 43, 47, 0.9);
|
||||
color: white;
|
||||
}
|
||||
|
||||
background-color: rgba(17, 18, 20, 0.95);
|
||||
&:after {
|
||||
border-color: rgb(43, 43, 43);
|
||||
&:first-child {
|
||||
border-top-left-radius: 2px;
|
||||
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(
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
import type {ReactElement} from 'react'
|
||||
import React from 'react'
|
||||
import type {IconType} from 'react-icons'
|
||||
import {Group, Button} from 'reakit'
|
||||
import styled from 'styled-components'
|
||||
import {Button} from 'reakit'
|
||||
import ButtonImpl from './ToolbarIconButton'
|
||||
|
||||
const Opt = styled(ButtonImpl)``
|
||||
import Container from './ToolbarSwitchSelectContainer'
|
||||
|
||||
function OptionButton<T>({
|
||||
value,
|
||||
|
@ -22,7 +20,7 @@ function OptionButton<T>({
|
|||
}) {
|
||||
return (
|
||||
<>
|
||||
<Opt
|
||||
<ButtonImpl
|
||||
forwardedAs={Button}
|
||||
className={isSelected ? 'selected' : undefined}
|
||||
aria-label={label}
|
||||
|
@ -30,7 +28,7 @@ function OptionButton<T>({
|
|||
title={label}
|
||||
>
|
||||
{icon}
|
||||
</Opt>
|
||||
</ButtonImpl>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -45,11 +43,6 @@ interface Props<Option> {
|
|||
}[]
|
||||
}
|
||||
|
||||
const Container = styled(Group)`
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
`
|
||||
|
||||
const ToolbarSwitchSelect = <Option extends string | number>({
|
||||
value: valueOfSwitch,
|
||||
onChange,
|
||||
|
|
|
@ -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
|
34
theatre/studio/src/uiComponents/useHotspot.ts
Normal file
34
theatre/studio/src/uiComponents/useHotspot.ts
Normal 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
|
||||
}
|
Loading…
Reference in a new issue