Merge pull request #39 from tldraw/36-refactor-design-system
36 refactor design system, add dark mode
This commit is contained in:
commit
63ffc27b5d
40 changed files with 1017 additions and 528 deletions
|
@ -52,6 +52,6 @@ const StyledCorner = styled('rect', {
|
|||
|
||||
const StyledCornerInner = styled('rect', {
|
||||
stroke: '$bounds',
|
||||
fill: '#fff',
|
||||
fill: '$panel',
|
||||
zStrokeWidth: 1.5,
|
||||
})
|
||||
|
|
|
@ -2,9 +2,15 @@ import * as _ContextMenu from '@radix-ui/react-context-menu'
|
|||
import styled from 'styles'
|
||||
import {
|
||||
IconWrapper,
|
||||
IconButton as _IconButton,
|
||||
RowButton,
|
||||
breakpoints,
|
||||
RowButton,
|
||||
ContextMenuArrow,
|
||||
ContextMenuDivider,
|
||||
ContextMenuButton,
|
||||
ContextMenuSubMenu,
|
||||
ContextMenuIconButton,
|
||||
ContextMenuRoot,
|
||||
MenuContent,
|
||||
} from 'components/shared'
|
||||
import { commandKey, deepCompareArrays, isMobile } from 'utils'
|
||||
import state, { useSelector } from 'state'
|
||||
|
@ -93,60 +99,64 @@ export default function ContextMenu({
|
|||
const hasThreeOrMore = selectedShapeIds.length > 2
|
||||
|
||||
return (
|
||||
<_ContextMenu.Root dir="ltr">
|
||||
<ContextMenuRoot>
|
||||
<_ContextMenu.Trigger>{children}</_ContextMenu.Trigger>
|
||||
<StyledContent ref={rContent} isMobile={isMobile()}>
|
||||
<MenuContent
|
||||
as={_ContextMenu.Content}
|
||||
ref={rContent}
|
||||
isMobile={isMobile()}
|
||||
>
|
||||
{selectedShapeIds.length ? (
|
||||
<>
|
||||
{/* <Button onSelect={() => state.send('COPIED')}>
|
||||
{/* <ContextMenuButton onSelect={() => state.send('COPIED')}>
|
||||
<span>Copy</span>
|
||||
<kbd>
|
||||
<span>{commandKey()}</span>
|
||||
<span>C</span>
|
||||
</kbd>
|
||||
</Button>
|
||||
<Button onSelect={() => state.send('CUT')}>
|
||||
</ContextMenuButton>
|
||||
<ContextMenuButton onSelect={() => state.send('CUT')}>
|
||||
<span>Cut</span>
|
||||
<kbd>
|
||||
<span>{commandKey()}</span>
|
||||
<span>X</span>
|
||||
</kbd>
|
||||
</Button>
|
||||
</ContextMenuButton>
|
||||
*/}
|
||||
<Button onSelect={() => state.send('DUPLICATED')}>
|
||||
<ContextMenuButton onSelect={() => state.send('DUPLICATED')}>
|
||||
<span>Duplicate</span>
|
||||
<kbd>
|
||||
<span>{commandKey()}</span>
|
||||
<span>D</span>
|
||||
</kbd>
|
||||
</Button>
|
||||
<StyledDivider />
|
||||
</ContextMenuButton>
|
||||
<ContextMenuDivider />
|
||||
{hasGroupSelected ||
|
||||
(hasTwoOrMore && (
|
||||
<>
|
||||
{hasGroupSelected && (
|
||||
<Button onSelect={() => state.send('UNGROUPED')}>
|
||||
<ContextMenuButton onSelect={() => state.send('UNGROUPED')}>
|
||||
<span>Ungroup</span>
|
||||
<kbd>
|
||||
<span>{commandKey()}</span>
|
||||
<span>⇧</span>
|
||||
<span>G</span>
|
||||
</kbd>
|
||||
</Button>
|
||||
</ContextMenuButton>
|
||||
)}
|
||||
{hasTwoOrMore && (
|
||||
<Button onSelect={() => state.send('GROUPED')}>
|
||||
<ContextMenuButton onSelect={() => state.send('GROUPED')}>
|
||||
<span>Group</span>
|
||||
<kbd>
|
||||
<span>{commandKey()}</span>
|
||||
<span>G</span>
|
||||
</kbd>
|
||||
</Button>
|
||||
</ContextMenuButton>
|
||||
)}
|
||||
</>
|
||||
))}
|
||||
<SubMenu label="Move">
|
||||
<Button
|
||||
<ContextMenuSubMenu label="Move">
|
||||
<ContextMenuButton
|
||||
onSelect={() =>
|
||||
state.send('MOVED', {
|
||||
type: MoveType.ToFront,
|
||||
|
@ -159,9 +169,9 @@ export default function ContextMenu({
|
|||
<span>⇧</span>
|
||||
<span>]</span>
|
||||
</kbd>
|
||||
</Button>
|
||||
</ContextMenuButton>
|
||||
|
||||
<Button
|
||||
<ContextMenuButton
|
||||
onSelect={() =>
|
||||
state.send('MOVED', {
|
||||
type: MoveType.Forward,
|
||||
|
@ -173,8 +183,8 @@ export default function ContextMenu({
|
|||
<span>{commandKey()}</span>
|
||||
<span>]</span>
|
||||
</kbd>
|
||||
</Button>
|
||||
<Button
|
||||
</ContextMenuButton>
|
||||
<ContextMenuButton
|
||||
onSelect={() =>
|
||||
state.send('MOVED', {
|
||||
type: MoveType.Backward,
|
||||
|
@ -186,8 +196,8 @@ export default function ContextMenu({
|
|||
<span>{commandKey()}</span>
|
||||
<span>[</span>
|
||||
</kbd>
|
||||
</Button>
|
||||
<Button
|
||||
</ContextMenuButton>
|
||||
<ContextMenuButton
|
||||
onSelect={() =>
|
||||
state.send('MOVED', {
|
||||
type: MoveType.ToBack,
|
||||
|
@ -200,8 +210,8 @@ export default function ContextMenu({
|
|||
<span>⇧</span>
|
||||
<span>[</span>
|
||||
</kbd>
|
||||
</Button>
|
||||
</SubMenu>
|
||||
</ContextMenuButton>
|
||||
</ContextMenuSubMenu>
|
||||
{hasTwoOrMore && (
|
||||
<AlignDistributeSubMenu
|
||||
hasTwoOrMore={hasTwoOrMore}
|
||||
|
@ -209,150 +219,43 @@ export default function ContextMenu({
|
|||
/>
|
||||
)}
|
||||
<MoveToPageMenu />
|
||||
<Button onSelect={() => state.send('COPIED_TO_SVG')}>
|
||||
<ContextMenuButton onSelect={() => state.send('COPIED_TO_SVG')}>
|
||||
<span>Copy to SVG</span>
|
||||
<kbd>
|
||||
<span>{commandKey()}</span>
|
||||
<span>⇧</span>
|
||||
<span>C</span>
|
||||
</kbd>
|
||||
</Button>
|
||||
<StyledDivider />
|
||||
<Button onSelect={() => state.send('DELETED')}>
|
||||
</ContextMenuButton>
|
||||
<ContextMenuDivider />
|
||||
<ContextMenuButton onSelect={() => state.send('DELETED')}>
|
||||
<span>Delete</span>
|
||||
<kbd>
|
||||
<span>⌫</span>
|
||||
</kbd>
|
||||
</Button>
|
||||
</ContextMenuButton>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button onSelect={() => state.send('UNDO')}>
|
||||
<ContextMenuButton onSelect={() => state.send('UNDO')}>
|
||||
<span>Undo</span>
|
||||
<kbd>
|
||||
<span>{commandKey()}</span>
|
||||
<span>Z</span>
|
||||
</kbd>
|
||||
</Button>
|
||||
<Button onSelect={() => state.send('REDO')}>
|
||||
</ContextMenuButton>
|
||||
<ContextMenuButton onSelect={() => state.send('REDO')}>
|
||||
<span>Redo</span>
|
||||
<kbd>
|
||||
<span>{commandKey()}</span>
|
||||
<span>⇧</span>
|
||||
<span>Z</span>
|
||||
</kbd>
|
||||
</Button>
|
||||
</ContextMenuButton>
|
||||
</>
|
||||
)}
|
||||
</StyledContent>
|
||||
</_ContextMenu.Root>
|
||||
)
|
||||
}
|
||||
|
||||
const StyledContent = styled(_ContextMenu.Content, {
|
||||
position: 'relative',
|
||||
backgroundColor: '$panel',
|
||||
borderRadius: '4px',
|
||||
overflow: 'hidden',
|
||||
pointerEvents: 'all',
|
||||
userSelect: 'none',
|
||||
zIndex: 200,
|
||||
padding: 3,
|
||||
boxShadow: '0px 2px 4px rgba(0,0,0,.2)',
|
||||
minWidth: 128,
|
||||
font: '$ui',
|
||||
|
||||
'& kbd': {
|
||||
marginLeft: '32px',
|
||||
fontSize: '$1',
|
||||
fontFamily: '$ui',
|
||||
fontWeight: 400,
|
||||
},
|
||||
|
||||
'& kbd > span': {
|
||||
display: 'inline-block',
|
||||
width: '12px',
|
||||
},
|
||||
|
||||
variants: {
|
||||
isMobile: {
|
||||
true: {
|
||||
'& kbd': {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const StyledDivider = styled(_ContextMenu.Separator, {
|
||||
backgroundColor: '$hover',
|
||||
height: 1,
|
||||
margin: '3px -3px',
|
||||
})
|
||||
|
||||
function Button({
|
||||
onSelect,
|
||||
children,
|
||||
disabled = false,
|
||||
}: {
|
||||
onSelect: () => void
|
||||
disabled?: boolean
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<_ContextMenu.Item
|
||||
as={RowButton}
|
||||
disabled={disabled}
|
||||
bp={breakpoints}
|
||||
onSelect={onSelect}
|
||||
>
|
||||
{children}
|
||||
</_ContextMenu.Item>
|
||||
)
|
||||
}
|
||||
|
||||
function IconButton({
|
||||
onSelect,
|
||||
children,
|
||||
disabled = false,
|
||||
}: {
|
||||
onSelect: () => void
|
||||
disabled?: boolean
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<_ContextMenu.Item
|
||||
as={_IconButton}
|
||||
bp={breakpoints}
|
||||
disabled={disabled}
|
||||
onSelect={onSelect}
|
||||
>
|
||||
{children}
|
||||
</_ContextMenu.Item>
|
||||
)
|
||||
}
|
||||
|
||||
function SubMenu({
|
||||
children,
|
||||
label,
|
||||
}: {
|
||||
label: string
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<_ContextMenu.Root dir="ltr">
|
||||
<_ContextMenu.TriggerItem as={RowButton} bp={breakpoints}>
|
||||
<span>{label}</span>
|
||||
<IconWrapper size="small">
|
||||
<ChevronRightIcon />
|
||||
</IconWrapper>
|
||||
</_ContextMenu.TriggerItem>
|
||||
<StyledContent sideOffset={2} alignOffset={-2} isMobile={isMobile()}>
|
||||
{children}
|
||||
<StyledArrow offset={13} />
|
||||
</StyledContent>
|
||||
</_ContextMenu.Root>
|
||||
</MenuContent>
|
||||
</ContextMenuRoot>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -363,7 +266,7 @@ function AlignDistributeSubMenu({
|
|||
hasThreeOrMore: boolean
|
||||
}) {
|
||||
return (
|
||||
<_ContextMenu.Root dir="ltr">
|
||||
<ContextMenuRoot>
|
||||
<_ContextMenu.TriggerItem as={RowButton} bp={breakpoints}>
|
||||
<span>Align / Distribute</span>
|
||||
<IconWrapper size="small">
|
||||
|
@ -371,53 +274,54 @@ function AlignDistributeSubMenu({
|
|||
</IconWrapper>
|
||||
</_ContextMenu.TriggerItem>
|
||||
<StyledGrid
|
||||
as={_ContextMenu.Content}
|
||||
sideOffset={2}
|
||||
alignOffset={-2}
|
||||
isMobile={isMobile()}
|
||||
selectedStyle={hasThreeOrMore ? 'threeOrMore' : 'twoOrMore'}
|
||||
>
|
||||
<IconButton onSelect={alignLeft}>
|
||||
<ContextMenuIconButton onSelect={alignLeft}>
|
||||
<AlignLeftIcon />
|
||||
</IconButton>
|
||||
<IconButton onSelect={alignCenterHorizontal}>
|
||||
</ContextMenuIconButton>
|
||||
<ContextMenuIconButton onSelect={alignCenterHorizontal}>
|
||||
<AlignCenterHorizontallyIcon />
|
||||
</IconButton>
|
||||
<IconButton onSelect={alignRight}>
|
||||
</ContextMenuIconButton>
|
||||
<ContextMenuIconButton onSelect={alignRight}>
|
||||
<AlignRightIcon />
|
||||
</IconButton>
|
||||
<IconButton onSelect={stretchHorizontally}>
|
||||
</ContextMenuIconButton>
|
||||
<ContextMenuIconButton onSelect={stretchHorizontally}>
|
||||
<StretchHorizontallyIcon />
|
||||
</IconButton>
|
||||
</ContextMenuIconButton>
|
||||
{hasThreeOrMore && (
|
||||
<IconButton onSelect={distributeHorizontally}>
|
||||
<ContextMenuIconButton onSelect={distributeHorizontally}>
|
||||
<SpaceEvenlyHorizontallyIcon />
|
||||
</IconButton>
|
||||
</ContextMenuIconButton>
|
||||
)}
|
||||
|
||||
<IconButton onSelect={alignTop}>
|
||||
<ContextMenuIconButton onSelect={alignTop}>
|
||||
<AlignTopIcon />
|
||||
</IconButton>
|
||||
<IconButton onSelect={alignCenterVertical}>
|
||||
</ContextMenuIconButton>
|
||||
<ContextMenuIconButton onSelect={alignCenterVertical}>
|
||||
<AlignCenterVerticallyIcon />
|
||||
</IconButton>
|
||||
<IconButton onSelect={alignBottom}>
|
||||
</ContextMenuIconButton>
|
||||
<ContextMenuIconButton onSelect={alignBottom}>
|
||||
<AlignBottomIcon />
|
||||
</IconButton>
|
||||
<IconButton onSelect={stretchVertically}>
|
||||
</ContextMenuIconButton>
|
||||
<ContextMenuIconButton onSelect={stretchVertically}>
|
||||
<StretchVerticallyIcon />
|
||||
</IconButton>
|
||||
</ContextMenuIconButton>
|
||||
{hasThreeOrMore && (
|
||||
<IconButton onSelect={distributeVertically}>
|
||||
<ContextMenuIconButton onSelect={distributeVertically}>
|
||||
<SpaceEvenlyVerticallyIcon />
|
||||
</IconButton>
|
||||
</ContextMenuIconButton>
|
||||
)}
|
||||
<StyledArrow offset={13} />
|
||||
<ContextMenuArrow offset={13} />
|
||||
</StyledGrid>
|
||||
</_ContextMenu.Root>
|
||||
</ContextMenuRoot>
|
||||
)
|
||||
}
|
||||
|
||||
const StyledGrid = styled(StyledContent, {
|
||||
const StyledGrid = styled(MenuContent, {
|
||||
display: 'grid',
|
||||
variants: {
|
||||
selectedStyle: {
|
||||
|
@ -444,29 +348,30 @@ function MoveToPageMenu() {
|
|||
if (sorted.length === 0) return null
|
||||
|
||||
return (
|
||||
<_ContextMenu.Root dir="ltr">
|
||||
<_ContextMenu.TriggerItem as={RowButton} bp={breakpoints}>
|
||||
<ContextMenuRoot>
|
||||
<ContextMenuButton>
|
||||
<span>Move To Page</span>
|
||||
<IconWrapper size="small">
|
||||
<ChevronRightIcon />
|
||||
</IconWrapper>
|
||||
</_ContextMenu.TriggerItem>
|
||||
<StyledContent sideOffset={2} alignOffset={-2} isMobile={isMobile()}>
|
||||
</ContextMenuButton>
|
||||
<MenuContent
|
||||
as={_ContextMenu.Content}
|
||||
sideOffset={2}
|
||||
alignOffset={-2}
|
||||
isMobile={isMobile()}
|
||||
>
|
||||
{sorted.map(({ id, name }) => (
|
||||
<Button
|
||||
<ContextMenuButton
|
||||
key={id}
|
||||
disabled={id === currentPageId}
|
||||
onSelect={() => state.send('MOVED_TO_PAGE', { id })}
|
||||
>
|
||||
<span>{name}</span>
|
||||
</Button>
|
||||
</ContextMenuButton>
|
||||
))}
|
||||
<StyledArrow offset={13} />
|
||||
</StyledContent>
|
||||
</_ContextMenu.Root>
|
||||
<ContextMenuArrow offset={13} />
|
||||
</MenuContent>
|
||||
</ContextMenuRoot>
|
||||
)
|
||||
}
|
||||
|
||||
const StyledArrow = styled(_ContextMenu.Arrow, {
|
||||
fill: 'white',
|
||||
})
|
||||
|
|
|
@ -22,7 +22,7 @@ function HoveredShape({ id }: { id: string }) {
|
|||
|
||||
const strokeWidth = useSelector((s) => {
|
||||
const shape = tld.getShape(s.data, id)
|
||||
const style = getShapeStyle(shape.style)
|
||||
const style = getShapeStyle(shape.style, s.data.settings.isDarkMode)
|
||||
return +style.strokeWidth
|
||||
})
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ interface Node {
|
|||
isEditing: boolean
|
||||
isHovered: boolean
|
||||
isSelected: boolean
|
||||
isDarkMode: boolean
|
||||
isCurrentParent: boolean
|
||||
}
|
||||
|
||||
|
@ -65,7 +66,15 @@ interface ShapeNodeProps {
|
|||
}
|
||||
|
||||
const ShapeNode = ({
|
||||
node: { shape, children, isEditing, isHovered, isSelected, isCurrentParent },
|
||||
node: {
|
||||
shape,
|
||||
children,
|
||||
isEditing,
|
||||
isHovered,
|
||||
isDarkMode,
|
||||
isSelected,
|
||||
isCurrentParent,
|
||||
},
|
||||
}: ShapeNodeProps) => {
|
||||
return (
|
||||
<>
|
||||
|
@ -74,6 +83,7 @@ const ShapeNode = ({
|
|||
isEditing={isEditing}
|
||||
isHovered={isHovered}
|
||||
isSelected={isSelected}
|
||||
isDarkMode={isDarkMode}
|
||||
isCurrentParent={isCurrentParent}
|
||||
/>
|
||||
{children.map((childNode) => (
|
||||
|
@ -105,6 +115,7 @@ function addToTree(
|
|||
isHovered: data.hoveredId === shape.id,
|
||||
isCurrentParent: data.currentParentId === shape.id,
|
||||
isEditing: data.editingId === shape.id,
|
||||
isDarkMode: data.settings.isDarkMode,
|
||||
isSelected: selectedIds.includes(shape.id),
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ interface ShapeProps {
|
|||
isEditing: boolean
|
||||
isHovered: boolean
|
||||
isSelected: boolean
|
||||
isDarkMode: boolean
|
||||
isCurrentParent: boolean
|
||||
}
|
||||
|
||||
|
@ -19,6 +20,7 @@ const Shape = memo(
|
|||
isEditing,
|
||||
isHovered,
|
||||
isSelected,
|
||||
isDarkMode,
|
||||
isCurrentParent,
|
||||
}: ShapeProps) => {
|
||||
const rGroup = useRef<SVGGElement>(null)
|
||||
|
@ -39,13 +41,14 @@ const Shape = memo(
|
|||
{...events}
|
||||
>
|
||||
{isEditing && shape.type === ShapeType.Text ? (
|
||||
<EditingTextShape shape={shape} />
|
||||
<EditingTextShape shape={shape} isDarkMode={isDarkMode} />
|
||||
) : (
|
||||
<RenderedShape
|
||||
shape={shape}
|
||||
isEditing={isEditing}
|
||||
isHovered={isHovered}
|
||||
isSelected={isSelected}
|
||||
isDarkMode={isDarkMode}
|
||||
isCurrentParent={isCurrentParent}
|
||||
/>
|
||||
)}
|
||||
|
@ -62,6 +65,7 @@ interface RenderedShapeProps {
|
|||
isEditing: boolean
|
||||
isHovered: boolean
|
||||
isSelected: boolean
|
||||
isDarkMode: boolean
|
||||
isCurrentParent: boolean
|
||||
}
|
||||
|
||||
|
@ -71,12 +75,14 @@ const RenderedShape = memo(
|
|||
isEditing,
|
||||
isHovered,
|
||||
isSelected,
|
||||
isDarkMode,
|
||||
isCurrentParent,
|
||||
}: RenderedShapeProps) {
|
||||
return getShapeUtils(shape).render(shape, {
|
||||
isEditing,
|
||||
isHovered,
|
||||
isSelected,
|
||||
isDarkMode,
|
||||
isCurrentParent,
|
||||
})
|
||||
},
|
||||
|
@ -85,6 +91,7 @@ const RenderedShape = memo(
|
|||
prev.isEditing !== next.isEditing ||
|
||||
prev.isHovered !== next.isHovered ||
|
||||
prev.isSelected !== next.isSelected ||
|
||||
prev.isDarkMode !== next.isDarkMode ||
|
||||
prev.isCurrentParent !== next.isCurrentParent
|
||||
) {
|
||||
return false
|
||||
|
@ -98,7 +105,13 @@ const RenderedShape = memo(
|
|||
}
|
||||
)
|
||||
|
||||
function EditingTextShape({ shape }: { shape: TextShape }) {
|
||||
function EditingTextShape({
|
||||
shape,
|
||||
isDarkMode,
|
||||
}: {
|
||||
shape: TextShape
|
||||
isDarkMode: boolean
|
||||
}) {
|
||||
const ref = useRef<HTMLTextAreaElement>(null)
|
||||
|
||||
return getShapeUtils(shape).render(shape, {
|
||||
|
@ -106,6 +119,7 @@ function EditingTextShape({ shape }: { shape: TextShape }) {
|
|||
isEditing: true,
|
||||
isHovered: false,
|
||||
isSelected: false,
|
||||
isDarkMode,
|
||||
isCurrentParent: false,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -113,7 +113,7 @@ const StyledDocs = styled('div', {
|
|||
},
|
||||
|
||||
'& blockquote': {
|
||||
backgroundColor: 'rgba(144, 144, 144, .05)',
|
||||
backgroundColor: '$overlay',
|
||||
padding: 12,
|
||||
margin: '20px 0',
|
||||
borderRadius: 8,
|
||||
|
|
|
@ -6,6 +6,7 @@ import React, { useCallback, useEffect, useRef } from 'react'
|
|||
import styled from 'styles'
|
||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'
|
||||
import { getFormattedCode } from 'utils/code'
|
||||
import { metaKey } from 'utils'
|
||||
|
||||
export type IMonaco = typeof monaco
|
||||
|
||||
|
@ -40,6 +41,7 @@ export default function CodeEditor({
|
|||
onKey,
|
||||
}: Props): JSX.Element {
|
||||
const { theme } = useTheme()
|
||||
|
||||
const rEditor = useRef<IMonacoEditor>(null)
|
||||
const rMonaco = useRef<IMonaco>(null)
|
||||
|
||||
|
@ -135,11 +137,14 @@ export default function CodeEditor({
|
|||
const handleKeydown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation()
|
||||
|
||||
!modifierKeys.includes(e.key) && onKey?.()
|
||||
const metaKey = navigator.platform.match('Mac') ? e.metaKey : e.ctrlKey
|
||||
if (e.key === 's' && metaKey) {
|
||||
|
||||
if ((e.key === 's' || e.key === 'Enter') && metaKey(e)) {
|
||||
const editor = rEditor.current
|
||||
|
||||
if (!editor) return
|
||||
|
||||
editor
|
||||
.getAction('editor.action.formatDocument')
|
||||
.run()
|
||||
|
@ -149,10 +154,11 @@ export default function CodeEditor({
|
|||
|
||||
e.preventDefault()
|
||||
}
|
||||
if (e.key === 'p' && metaKey) {
|
||||
if (e.key === 'p' && metaKey(e)) {
|
||||
e.preventDefault()
|
||||
}
|
||||
if (e.key === 'd' && metaKey) {
|
||||
|
||||
if (e.key === 'd' && metaKey(e)) {
|
||||
e.preventDefault()
|
||||
}
|
||||
},
|
||||
|
|
|
@ -185,17 +185,17 @@ const StylePanelRoot = styled(Panel.Root, {
|
|||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
border: '1px solid $panel',
|
||||
boxShadow: '0px 2px 4px rgba(0,0,0,.2)',
|
||||
boxShadow: '$4',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
pointerEvents: 'all',
|
||||
padding: 2,
|
||||
padding: '$0',
|
||||
|
||||
'& hr': {
|
||||
marginTop: 2,
|
||||
marginBottom: 2,
|
||||
marginLeft: '-2px',
|
||||
marginLeft: '-$0',
|
||||
border: 'none',
|
||||
height: 1,
|
||||
backgroundColor: '$brushFill',
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import useKeyboardEvents from 'hooks/useKeyboardEvents'
|
||||
import useLoadOnMount from 'hooks/useLoadOnMount'
|
||||
import Menu from './menu/menu'
|
||||
import Canvas from './canvas/canvas'
|
||||
import StatusBar from './status-bar'
|
||||
import ToolsPanel from './tools-panel/tools-panel'
|
||||
|
@ -16,9 +17,12 @@ export default function Editor({ roomId }: { roomId?: string }): JSX.Element {
|
|||
|
||||
return (
|
||||
<Layout>
|
||||
<DebugPanel />
|
||||
<CodePanel />
|
||||
<PagePanel />
|
||||
<MenuButtons>
|
||||
<Menu />
|
||||
<DebugPanel />
|
||||
<CodePanel />
|
||||
<PagePanel />
|
||||
</MenuButtons>
|
||||
<ControlsPanel />
|
||||
<Spacer />
|
||||
<StylePanel />
|
||||
|
@ -33,6 +37,11 @@ const Spacer = styled('div', {
|
|||
flexGrow: 2,
|
||||
})
|
||||
|
||||
const MenuButtons = styled('div', {
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
})
|
||||
|
||||
const Layout = styled('main', {
|
||||
position: 'fixed',
|
||||
overflow: 'hidden',
|
||||
|
|
25
components/icons/check.tsx
Normal file
25
components/icons/check.tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import * as React from 'react'
|
||||
|
||||
function SvgCheck(props: React.SVGProps<SVGSVGElement>): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 15 15"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M1.06555 6.39147C1.06555 6.39147 1.18669 6.37676 1.32187 6.60547C1.41196 6.75789 1.43336 6.79297 1.54896 6.97656C1.67649 7.1791 1.90215 7.46571 1.98273 7.55604C1.89684 7.45413 2.06332 7.64638 1.98273 7.55604C2.06863 7.65796 2.15476 7.76164 2.24114 7.86709C2.32752 7.97255 2.43047 8.09537 2.54999 8.23556C2.66951 8.37575 2.82609 8.54986 3.01971 8.75788C3.21333 8.96589 3.41752 9.17647 3.63228 9.38961C3.43268 9.19463 3.84705 9.60274 3.63228 9.38961C3.83189 9.58458 3.98492 9.65483 3.97262 9.74102C4.18567 9.85603 3.96031 9.82721 3.97262 9.74102C3.91266 9.99944 3.75956 9.626 3.97262 9.74102C4.03257 9.4826 4.10858 9.33351 4.21165 9.13551C4.31473 8.93751 4.42218 8.73459 4.534 8.52675C4.64583 8.31892 4.76102 8.10882 4.87957 7.89646C4.99812 7.68411 5.13449 7.4489 5.28871 7.19083C5.44292 6.93276 5.63515 6.62674 5.86541 6.27278C6.09567 5.91882 6.34391 5.55345 6.61015 5.17668C6.87639 4.7999 7.15756 4.4262 7.45365 4.05557C7.74975 3.68495 8.06403 3.32626 8.39649 2.97952C8.72895 2.63277 9.04339 2.32894 9.33982 2.06804C9.63624 1.80713 9.87576 1.60935 10.0584 1.4747C10.241 1.34004 10.4399 1.20541 10.655 1.07079C10.8702 0.936165 10.9439 0.993562 10.8761 1.24297C10.8084 1.49239 10.7342 1.72411 10.6535 1.93813C10.5728 2.15215 10.452 2.43667 10.291 2.79168C10.13 3.14669 9.94264 3.53252 9.7288 3.94917C9.51497 4.36582 9.29528 4.77352 9.06973 5.17228C8.84418 5.57104 8.62072 5.96246 8.39936 6.34654C8.178 6.73061 7.9685 7.0987 7.77088 7.45081C7.57326 7.80292 7.40426 8.10581 7.26389 8.35948C7.12353 8.61314 6.99519 8.84594 6.87889 9.05785C6.76259 9.26977 6.64515 9.48087 6.52659 9.69115C6.40803 9.90143 6.25752 10.1532 6.07507 10.4466C5.89262 10.7399 5.72254 11.0063 5.56483 11.2458C5.40712 11.4852 5.23545 11.6777 5.04981 11.8232C4.86416 11.9686 4.59686 12.0243 4.2479 11.9903C3.89894 11.9563 3.61525 11.8614 3.39684 11.7055C3.17844 11.5497 2.99758 11.3507 2.85427 11.1085C2.71096 10.8663 2.56178 10.5997 2.40673 10.3088C2.25167 10.0179 2.11187 9.72784 1.98731 9.4386C1.86275 9.14937 1.76835 8.91301 1.70411 8.72952C1.63988 8.54604 1.58816 8.38956 1.54896 8.26008C1.50977 8.13061 1.47123 8.0037 1.43336 7.87934C1.39549 7.75498 1.35833 7.63676 1.32187 7.52466C1.28541 7.41257 1.24607 7.2993 1.20384 7.18486C1.16161 7.07041 1.21291 7.20988 1.06555 6.85296C0.918183 6.49603 1.06555 6.39147 1.06555 6.39147Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M1.98273 7.55604C1.90215 7.46571 1.67649 7.1791 1.54896 6.97656C1.43336 6.79297 1.41196 6.75789 1.32187 6.60547C1.18669 6.37676 1.06555 6.39147 1.06555 6.39147C1.06555 6.39147 0.918183 6.49603 1.06555 6.85296C1.21291 7.20988 1.16161 7.07041 1.20384 7.18486C1.24607 7.2993 1.28541 7.41257 1.32187 7.52466C1.35833 7.63676 1.39549 7.75498 1.43336 7.87934C1.47123 8.0037 1.50977 8.13061 1.54896 8.26008C1.58816 8.38956 1.63988 8.54604 1.70411 8.72952C1.76835 8.91301 1.86275 9.14937 1.98731 9.4386C2.11187 9.72784 2.25167 10.0179 2.40673 10.3088C2.56178 10.5997 2.71096 10.8663 2.85427 11.1085C2.99758 11.3507 3.17844 11.5497 3.39684 11.7055C3.61525 11.8614 3.89894 11.9563 4.2479 11.9903C4.59686 12.0243 4.86416 11.9686 5.04981 11.8232C5.23545 11.6777 5.40712 11.4852 5.56483 11.2458C5.72254 11.0063 5.89262 10.7399 6.07507 10.4466C6.25752 10.1532 6.40803 9.90143 6.52659 9.69115C6.64515 9.48087 6.76259 9.26977 6.87889 9.05785C6.99519 8.84594 7.12353 8.61314 7.26389 8.35948C7.40426 8.10581 7.57326 7.80292 7.77088 7.45081C7.9685 7.0987 8.178 6.73061 8.39936 6.34654C8.62072 5.96246 8.84418 5.57104 9.06973 5.17228C9.29528 4.77352 9.51497 4.36582 9.7288 3.94917C9.94264 3.53252 10.13 3.14669 10.291 2.79168C10.452 2.43667 10.5728 2.15215 10.6535 1.93813C10.7342 1.72411 10.8084 1.49239 10.8761 1.24297C10.9439 0.993562 10.8702 0.936165 10.655 1.07079C10.4399 1.20541 10.241 1.34004 10.0584 1.4747C9.87576 1.60935 9.63624 1.80713 9.33982 2.06804C9.04339 2.32894 8.72895 2.63277 8.39649 2.97952C8.06403 3.32626 7.74975 3.68495 7.45365 4.05557C7.15756 4.4262 6.87639 4.7999 6.61015 5.17668C6.34391 5.55345 6.09567 5.91882 5.86541 6.27278C5.63515 6.62674 5.44292 6.93276 5.28871 7.19083C5.13449 7.4489 4.99812 7.68411 4.87957 7.89646C4.76102 8.10882 4.64583 8.31892 4.534 8.52675C4.42218 8.73459 4.31473 8.93751 4.21165 9.13551C4.10858 9.33351 4.03257 9.4826 3.97262 9.74102M1.98273 7.55604C2.06332 7.64638 1.89684 7.45413 1.98273 7.55604ZM1.98273 7.55604C2.06863 7.65796 2.15476 7.76164 2.24114 7.86709C2.32752 7.97255 2.43047 8.09537 2.54999 8.23556C2.66951 8.37575 2.82609 8.54986 3.01971 8.75788C3.21333 8.96589 3.41752 9.17647 3.63228 9.38961M3.63228 9.38961C3.84705 9.60274 3.43268 9.19463 3.63228 9.38961ZM3.63228 9.38961C3.83189 9.58458 3.98492 9.65483 3.97262 9.74102M3.97262 9.74102C3.96031 9.82721 4.18567 9.85603 3.97262 9.74102ZM3.97262 9.74102C3.75956 9.626 3.91266 9.99944 3.97262 9.74102Z"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default SvgCheck
|
|
@ -1,3 +1,4 @@
|
|||
export { default as Redo } from './redo'
|
||||
export { default as Trash } from './trash'
|
||||
export { default as Undo } from './undo'
|
||||
export { default as Check } from './check'
|
||||
|
|
|
@ -4,7 +4,7 @@ function SvgRedo(props: React.SVGProps<SVGSVGElement>): JSX.Element {
|
|||
return (
|
||||
<svg
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
|
@ -12,13 +12,11 @@ function SvgRedo(props: React.SVGProps<SVGSVGElement>): JSX.Element {
|
|||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M12.5 2.495a.5.5 0 00-.5.5v2.5H9.5a.5.5 0 100 1h3a.5.5 0 00.5-.5v-3a.5.5 0 00-.5-.5z"
|
||||
fill="#000"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M7.697 2.049a5 5 0 104.02 6.613.5.5 0 10-.944-.332 4 4 0 11-.946-4.16l.01.01 2.32 2.18a.5.5 0 00.685-.729l-2.314-2.175A5 5 0 007.697 2.05z"
|
||||
fill="#000"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
|
|
@ -4,7 +4,7 @@ function SvgTrash(props: React.SVGProps<SVGSVGElement>): JSX.Element {
|
|||
return (
|
||||
<svg
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
|
@ -12,19 +12,16 @@ function SvgTrash(props: React.SVGProps<SVGSVGElement>): JSX.Element {
|
|||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M2 4.656a.5.5 0 01.5-.5h9.7a.5.5 0 010 1H2.5a.5.5 0 01-.5-.5z"
|
||||
fill="#000"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M6.272 3a.578.578 0 00-.578.578v.578h3.311v-.578A.578.578 0 008.428 3H6.272zm3.733 1.156v-.578A1.578 1.578 0 008.428 2H6.272a1.578 1.578 0 00-1.578 1.578v.578H3.578a.5.5 0 00-.5.5V12.2a1.578 1.578 0 001.577 1.578h5.39a1.578 1.578 0 001.577-1.578V4.656a.5.5 0 00-.5-.5h-1.117zm-5.927 1V12.2a.578.578 0 00.577.578h5.39a.578.578 0 00.577-.578V5.156H4.078z"
|
||||
fill="#000"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M6.272 6.85a.5.5 0 01.5.5v3.233a.5.5 0 11-1 0V7.35a.5.5 0 01.5-.5zM8.428 6.85a.5.5 0 01.5.5v3.233a.5.5 0 11-1 0V7.35a.5.5 0 01.5-.5z"
|
||||
fill="#000"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
|
|
@ -4,7 +4,7 @@ function SvgUndo(props: React.SVGProps<SVGSVGElement>): JSX.Element {
|
|||
return (
|
||||
<svg
|
||||
viewBox="0 0 15 15"
|
||||
fill="none"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
|
@ -12,13 +12,11 @@ function SvgUndo(props: React.SVGProps<SVGSVGElement>): JSX.Element {
|
|||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M2.5 2.495a.5.5 0 01.5.5v2.5h2.5a.5.5 0 110 1h-3a.5.5 0 01-.5-.5v-3a.5.5 0 01.5-.5z"
|
||||
fill="#000"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M7.303 2.049a5 5 0 11-4.02 6.613.5.5 0 01.944-.332 4 4 0 10.946-4.16l-.01.01-2.32 2.18a.5.5 0 01-.685-.729l2.314-2.175A5 5 0 017.303 2.05z"
|
||||
fill="#000"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
|
103
components/menu/menu.tsx
Normal file
103
components/menu/menu.tsx
Normal file
|
@ -0,0 +1,103 @@
|
|||
import * as React from 'react'
|
||||
import { HamburgerMenuIcon } from '@radix-ui/react-icons'
|
||||
import { Trigger, Content } from '@radix-ui/react-dropdown-menu'
|
||||
import { memo } from 'react'
|
||||
import {
|
||||
FloatingContainer,
|
||||
DropdownMenuRoot,
|
||||
MenuContent,
|
||||
IconButton,
|
||||
breakpoints,
|
||||
DropdownMenuButton,
|
||||
DropdownMenuSubMenu,
|
||||
DropdownMenuDivider,
|
||||
DropdownMenuCheckboxItem,
|
||||
} from '../shared'
|
||||
import state, { useSelector } from 'state'
|
||||
import { commandKey } from 'utils'
|
||||
|
||||
const handleNew = () => state.send('CREATED_NEW_PROJECT')
|
||||
const handleSave = () => state.send('SAVED')
|
||||
const handleLoad = () => state.send('LOADED_FROM_FILE_STSTEM')
|
||||
const toggleDarkMode = () => state.send('TOGGLED_DARK_MODE')
|
||||
|
||||
function Menu() {
|
||||
return (
|
||||
<FloatingContainer>
|
||||
<DropdownMenuRoot>
|
||||
<IconButton as={Trigger} bp={breakpoints}>
|
||||
<HamburgerMenuIcon />
|
||||
</IconButton>
|
||||
<Content as={MenuContent} sideOffset={8}>
|
||||
<DropdownMenuButton onSelect={handleNew} disabled>
|
||||
<span>New Project</span>
|
||||
<kbd>
|
||||
<span>{commandKey()}</span>
|
||||
<span>N</span>
|
||||
</kbd>
|
||||
</DropdownMenuButton>
|
||||
<DropdownMenuDivider />
|
||||
<DropdownMenuButton onSelect={handleLoad}>
|
||||
<span>Open...</span>
|
||||
<kbd>
|
||||
<span>{commandKey()}</span>
|
||||
<span>L</span>
|
||||
</kbd>
|
||||
</DropdownMenuButton>
|
||||
<RecentFiles />
|
||||
<DropdownMenuDivider />
|
||||
<DropdownMenuButton onSelect={handleSave}>
|
||||
<span>Save</span>
|
||||
<kbd>
|
||||
<span>{commandKey()}</span>
|
||||
<span>S</span>
|
||||
</kbd>
|
||||
</DropdownMenuButton>
|
||||
<DropdownMenuButton onSelect={handleSave}>
|
||||
<span>Save As...</span>
|
||||
<kbd>
|
||||
<span>⇧</span>
|
||||
<span>{commandKey()}</span>
|
||||
<span>S</span>
|
||||
</kbd>
|
||||
</DropdownMenuButton>
|
||||
<DropdownMenuDivider />
|
||||
<Preferences />
|
||||
</Content>
|
||||
</DropdownMenuRoot>
|
||||
</FloatingContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Menu)
|
||||
|
||||
function RecentFiles() {
|
||||
return (
|
||||
<DropdownMenuSubMenu label="Open Recent..." disabled>
|
||||
<DropdownMenuButton>
|
||||
<span>Project A</span>
|
||||
</DropdownMenuButton>
|
||||
<DropdownMenuButton>
|
||||
<span>Project B</span>
|
||||
</DropdownMenuButton>
|
||||
<DropdownMenuButton>
|
||||
<span>Project C</span>
|
||||
</DropdownMenuButton>
|
||||
</DropdownMenuSubMenu>
|
||||
)
|
||||
}
|
||||
|
||||
function Preferences() {
|
||||
const isDarkMode = useSelector((s) => s.data.settings.isDarkMode)
|
||||
|
||||
return (
|
||||
<DropdownMenuSubMenu label="Preferences">
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={isDarkMode}
|
||||
onCheckedChange={toggleDarkMode}
|
||||
>
|
||||
<span>Dark Mode</span>
|
||||
</DropdownMenuCheckboxItem>
|
||||
</DropdownMenuSubMenu>
|
||||
)
|
||||
}
|
|
@ -1,10 +1,16 @@
|
|||
import styled from 'styles'
|
||||
import * as ContextMenu from '@radix-ui/react-context-menu'
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
|
||||
import { breakpoints, IconWrapper, RowButton } from 'components/shared'
|
||||
import { CheckIcon, PlusIcon } from '@radix-ui/react-icons'
|
||||
import * as Panel from '../panel'
|
||||
import styled from 'styles'
|
||||
import {
|
||||
breakpoints,
|
||||
DropdownMenuButton,
|
||||
DropdownMenuDivider,
|
||||
RowButton,
|
||||
MenuContent,
|
||||
FloatingContainer,
|
||||
IconButton,
|
||||
IconWrapper,
|
||||
} from 'components/shared'
|
||||
import { MixerVerticalIcon, PlusIcon, CheckIcon } from '@radix-ui/react-icons'
|
||||
import state, { useSelector } from 'state'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
|
@ -37,153 +43,62 @@ export default function PagePanel(): JSX.Element {
|
|||
}
|
||||
}}
|
||||
>
|
||||
<PanelRoot dir="ltr">
|
||||
<DropdownMenu.Trigger
|
||||
as={RowButton}
|
||||
bp={breakpoints}
|
||||
variant="pageButton"
|
||||
>
|
||||
<FloatingContainer>
|
||||
<RowButton as={DropdownMenu.Trigger} bp={breakpoints}>
|
||||
<span>{documentPages[currentPageId].name}</span>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content sideOffset={8}>
|
||||
<PanelRoot>
|
||||
<DropdownMenu.RadioGroup
|
||||
as={Content}
|
||||
value={currentPageId}
|
||||
onValueChange={(id) => {
|
||||
setIsOpen(false)
|
||||
state.send('CHANGED_PAGE', { id })
|
||||
}}
|
||||
>
|
||||
{sorted.map(({ id, name }) => (
|
||||
<ContextMenu.Root dir="ltr" key={id}>
|
||||
<ContextMenu.Trigger>
|
||||
<StyledRadioItem key={id} value={id} bp={breakpoints}>
|
||||
<span>{name}</span>
|
||||
<DropdownMenu.ItemIndicator as={IconWrapper} size="small">
|
||||
<CheckIcon />
|
||||
</DropdownMenu.ItemIndicator>
|
||||
</StyledRadioItem>
|
||||
</ContextMenu.Trigger>
|
||||
<StyledContextMenuContent>
|
||||
<ContextMenu.Group>
|
||||
<StyledContextMenuItem
|
||||
onSelect={() => state.send('RENAMED_PAGE', { id })}
|
||||
>
|
||||
Rename
|
||||
</StyledContextMenuItem>
|
||||
<StyledContextMenuItem
|
||||
onSelect={() => {
|
||||
setIsOpen(false)
|
||||
state.send('DELETED_PAGE', { id })
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</StyledContextMenuItem>
|
||||
</ContextMenu.Group>
|
||||
</StyledContextMenuContent>
|
||||
</ContextMenu.Root>
|
||||
))}
|
||||
</DropdownMenu.RadioGroup>
|
||||
<DropdownMenu.Separator />
|
||||
<RowButton
|
||||
bp={breakpoints}
|
||||
onClick={() => {
|
||||
setIsOpen(false)
|
||||
state.send('CREATED_PAGE')
|
||||
}}
|
||||
>
|
||||
<span>Create Page</span>
|
||||
<IconWrapper size="small">
|
||||
<PlusIcon />
|
||||
</IconWrapper>
|
||||
</RowButton>
|
||||
</PanelRoot>
|
||||
</DropdownMenu.Content>
|
||||
</PanelRoot>
|
||||
</RowButton>
|
||||
</FloatingContainer>
|
||||
<MenuContent as={DropdownMenu.Content} sideOffset={8}>
|
||||
<DropdownMenu.RadioGroup
|
||||
value={currentPageId}
|
||||
onChange={(id) => {
|
||||
setIsOpen(false)
|
||||
state.send('CHANGED_PAGE', { id })
|
||||
}}
|
||||
>
|
||||
{sorted.map(({ id, name }) => (
|
||||
<ButtonWithOptions key={id}>
|
||||
<DropdownMenu.RadioItem
|
||||
as={RowButton}
|
||||
bp={breakpoints}
|
||||
value={id}
|
||||
variant="pageButton"
|
||||
>
|
||||
<span>{name}</span>
|
||||
<DropdownMenu.ItemIndicator>
|
||||
<IconWrapper>
|
||||
<CheckIcon />
|
||||
</IconWrapper>
|
||||
</DropdownMenu.ItemIndicator>
|
||||
</DropdownMenu.RadioItem>
|
||||
<IconButton bp={breakpoints} size="small" data-shy="true">
|
||||
<MixerVerticalIcon />
|
||||
</IconButton>
|
||||
</ButtonWithOptions>
|
||||
))}
|
||||
</DropdownMenu.RadioGroup>
|
||||
<DropdownMenuDivider />
|
||||
<DropdownMenuButton onSelect={() => state.send('CREATED_PAGE')}>
|
||||
<span>Create Page</span>
|
||||
<IconWrapper size="small">
|
||||
<PlusIcon />
|
||||
</IconWrapper>
|
||||
</DropdownMenuButton>
|
||||
</MenuContent>
|
||||
</DropdownMenu.Root>
|
||||
)
|
||||
}
|
||||
|
||||
const PanelRoot = styled('div', {
|
||||
marginLeft: 8,
|
||||
zIndex: 200,
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
pointerEvents: 'all',
|
||||
padding: '2px',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: '$panel',
|
||||
border: '1px solid $panel',
|
||||
boxShadow: '0px 2px 4px rgba(0,0,0,.2)',
|
||||
userSelect: 'none',
|
||||
})
|
||||
const ButtonWithOptions = styled('div', {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr auto',
|
||||
gridAutoFlow: 'column',
|
||||
|
||||
const Content = styled(Panel.Content, {
|
||||
width: '100%',
|
||||
minWidth: 128,
|
||||
})
|
||||
'& > *[data-shy="true"]': {
|
||||
opacity: 0,
|
||||
},
|
||||
|
||||
const StyledRadioItem = styled(DropdownMenu.RadioItem, {
|
||||
height: 32,
|
||||
width: 'auto',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '0 6px 0 12px',
|
||||
cursor: 'pointer',
|
||||
borderRadius: '4px',
|
||||
fontSize: '$1',
|
||||
fontFamily: '$ui',
|
||||
fontWeight: 400,
|
||||
backgroundColor: 'transparent',
|
||||
outline: 'none',
|
||||
variants: {
|
||||
bp: {
|
||||
mobile: {},
|
||||
small: {
|
||||
'&:hover': {
|
||||
backgroundColor: '$hover',
|
||||
},
|
||||
'&:focus-within': {
|
||||
backgroundColor: '$hover',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const StyledContextMenuContent = styled(ContextMenu.Content, {
|
||||
padding: '2px',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: '$panel',
|
||||
border: '1px solid $panel',
|
||||
boxShadow: '0px 2px 4px rgba(0,0,0,.2)',
|
||||
})
|
||||
|
||||
const StyledContextMenuItem = styled(ContextMenu.Item, {
|
||||
height: 32,
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '0 12px 0 12px',
|
||||
cursor: 'pointer',
|
||||
borderRadius: '4px',
|
||||
fontSize: '$1',
|
||||
fontFamily: '$ui',
|
||||
fontWeight: 400,
|
||||
backgroundColor: 'transparent',
|
||||
outline: 'none',
|
||||
bp: {
|
||||
mobile: {},
|
||||
small: {
|
||||
'&:hover:not(:disabled)': {
|
||||
backgroundColor: '$hover',
|
||||
},
|
||||
},
|
||||
'&:hover > *[data-shy="true"]': {
|
||||
opacity: 1,
|
||||
},
|
||||
})
|
||||
|
|
|
@ -9,7 +9,7 @@ export const Root = styled('div', {
|
|||
userSelect: 'none',
|
||||
zIndex: 200,
|
||||
border: '1px solid $panel',
|
||||
boxShadow: '0px 2px 4px rgba(0,0,0,.2)',
|
||||
boxShadow: '$4',
|
||||
font: '$ui',
|
||||
|
||||
variants: {
|
||||
|
@ -27,7 +27,7 @@ export const Root = styled('div', {
|
|||
isOpen: {
|
||||
true: {},
|
||||
false: {
|
||||
padding: 2,
|
||||
padding: '$0',
|
||||
height: 38,
|
||||
width: 38,
|
||||
},
|
||||
|
@ -84,7 +84,7 @@ export const Header = styled('div', {
|
|||
width: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: 2,
|
||||
padding: '$0',
|
||||
position: 'relative',
|
||||
|
||||
'& h3': {
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -35,12 +35,13 @@ const StatusBarContainer = styled('div', {
|
|||
zIndex: 300,
|
||||
height: 40,
|
||||
userSelect: 'none',
|
||||
borderTop: '1px solid black',
|
||||
borderTop: '1px solid $border',
|
||||
gridArea: 'status',
|
||||
display: 'grid',
|
||||
color: '$text',
|
||||
gridTemplateColumns: 'auto 1fr auto',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'white',
|
||||
backgroundColor: '$panel',
|
||||
gap: 8,
|
||||
fontSize: '$0',
|
||||
padding: '0 16px',
|
||||
|
|
|
@ -6,6 +6,7 @@ import { Square } from 'react-feather'
|
|||
import { DropdownContent } from '../shared'
|
||||
import { memo } from 'react'
|
||||
import state from 'state'
|
||||
import useTheme from 'hooks/useTheme'
|
||||
|
||||
function handleColorChange(
|
||||
e: Event & { currentTarget: { value: ColorStyle } }
|
||||
|
@ -14,9 +15,11 @@ function handleColorChange(
|
|||
}
|
||||
|
||||
function ColorContent(): JSX.Element {
|
||||
const { theme } = useTheme()
|
||||
|
||||
return (
|
||||
<DropdownContent sideOffset={8} side="bottom">
|
||||
{Object.keys(strokes).map((color: ColorStyle) => (
|
||||
{Object.keys(strokes[theme]).map((color: ColorStyle) => (
|
||||
<DropdownMenu.DropdownMenuItem
|
||||
as={IconButton}
|
||||
key={color}
|
||||
|
@ -24,7 +27,7 @@ function ColorContent(): JSX.Element {
|
|||
value={color}
|
||||
onSelect={handleColorChange}
|
||||
>
|
||||
<Square fill={strokes[color]} stroke="none" size="22" />
|
||||
<Square fill={strokes[theme][color]} stroke="none" size="22" />
|
||||
</DropdownMenu.DropdownMenuItem>
|
||||
))}
|
||||
</DropdownContent>
|
||||
|
|
|
@ -1,19 +1,21 @@
|
|||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import { breakpoints, IconButton } from 'components/shared'
|
||||
import Tooltip from 'components/tooltip'
|
||||
import { strokes } from 'state/shape-styles'
|
||||
import { fills, strokes } from 'state/shape-styles'
|
||||
import { useSelector } from 'state'
|
||||
import ColorContent from './color-content'
|
||||
import { BoxIcon } from '../shared'
|
||||
import useTheme from 'hooks/useTheme'
|
||||
|
||||
export default function QuickColorSelect(): JSX.Element {
|
||||
const color = useSelector((s) => s.values.selectedStyle.color)
|
||||
const { theme } = useTheme()
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root dir="ltr">
|
||||
<DropdownMenu.Trigger as={IconButton} bp={breakpoints}>
|
||||
<Tooltip label="Color">
|
||||
<BoxIcon fill={strokes[color]} stroke={strokes[color]} />
|
||||
<BoxIcon fill={fills[theme][color]} stroke={strokes[theme][color]} />
|
||||
</Tooltip>
|
||||
</DropdownMenu.Trigger>
|
||||
<ColorContent />
|
||||
|
|
|
@ -29,8 +29,6 @@ const handleStylePanelOpen = () => state.send('TOGGLED_STYLE_PANEL_OPEN')
|
|||
const handleCopy = () => state.send('COPIED')
|
||||
const handlePaste = () => state.send('PASTED')
|
||||
const handleCopyToSvg = () => state.send('COPIED_TO_SVG')
|
||||
const handleSave = () => state.send('SAVED')
|
||||
const handleLoad = () => state.send('LOADED_FROM_FILE_STSTEM')
|
||||
|
||||
export default function StylePanel(): JSX.Element {
|
||||
const rContainer = useRef<HTMLDivElement>(null)
|
||||
|
@ -99,13 +97,6 @@ function SelectedShapeContent(): JSX.Element {
|
|||
<Share2Icon />
|
||||
</IconWrapper>
|
||||
</RowButton>
|
||||
<hr />
|
||||
<RowButton bp={breakpoints} onClick={handleSave}>
|
||||
<span>Save</span>
|
||||
</RowButton>
|
||||
<RowButton bp={breakpoints} onClick={handleLoad}>
|
||||
<span>Load</span>
|
||||
</RowButton>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -117,18 +108,18 @@ const StylePanelRoot = styled(motion(Panel.Root), {
|
|||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
border: '1px solid $panel',
|
||||
boxShadow: '0px 2px 4px rgba(0,0,0,.2)',
|
||||
boxShadow: '$4',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
pointerEvents: 'all',
|
||||
padding: 2,
|
||||
padding: '$0',
|
||||
zIndex: 300,
|
||||
|
||||
'& hr': {
|
||||
marginTop: 2,
|
||||
marginBottom: 2,
|
||||
marginLeft: '-2px',
|
||||
marginLeft: '-$0',
|
||||
border: 'none',
|
||||
height: 1,
|
||||
backgroundColor: '$brushFill',
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { FloatingContainer } from 'components/shared'
|
||||
import Tooltip from 'components/tooltip'
|
||||
import styled from 'styles'
|
||||
|
||||
|
@ -5,6 +6,7 @@ export const ToolButton = styled('button', {
|
|||
position: 'relative',
|
||||
height: '32px',
|
||||
width: '32px',
|
||||
color: '$text',
|
||||
backgroundColor: '$panel',
|
||||
borderRadius: '4px',
|
||||
padding: '0',
|
||||
|
@ -219,21 +221,8 @@ export function TertiaryButton({
|
|||
)
|
||||
}
|
||||
|
||||
export const Container = styled('div', {
|
||||
backgroundColor: '$panel',
|
||||
border: '1px solid $panel',
|
||||
borderRadius: '4px',
|
||||
boxShadow: '0px 2px 4px rgba(0,0,0,.16)',
|
||||
display: 'flex',
|
||||
height: 'fit-content',
|
||||
padding: 2,
|
||||
pointerEvents: 'all',
|
||||
position: 'relative',
|
||||
userSelect: 'none',
|
||||
zIndex: 200,
|
||||
})
|
||||
|
||||
export const TertiaryButtonsContainer = styled(Container, {
|
||||
export const TertiaryButtonsContainer = styled(FloatingContainer, {
|
||||
boxShadow: '$3',
|
||||
variants: {
|
||||
bp: {
|
||||
mobile: {
|
||||
|
|
|
@ -8,7 +8,8 @@ import {
|
|||
SquareIcon,
|
||||
TextIcon,
|
||||
} from '@radix-ui/react-icons'
|
||||
import { PrimaryButton, SecondaryButton, Container } from './shared'
|
||||
import { PrimaryButton, SecondaryButton } from './shared'
|
||||
import { FloatingContainer } from '../shared'
|
||||
import React from 'react'
|
||||
import state, { useSelector } from 'state'
|
||||
import styled from 'styles'
|
||||
|
@ -33,7 +34,7 @@ export default function ToolsPanel(): JSX.Element {
|
|||
<ToolsPanelContainer>
|
||||
<LeftWrap size={{ '@initial': 'mobile', '@sm': 'small' }}>
|
||||
<Zoom />
|
||||
<Container>
|
||||
<FloatingContainer>
|
||||
<SecondaryButton
|
||||
label={'Select'}
|
||||
onClick={selectSelectTool}
|
||||
|
@ -41,10 +42,10 @@ export default function ToolsPanel(): JSX.Element {
|
|||
>
|
||||
<CursorArrowIcon />
|
||||
</SecondaryButton>
|
||||
</Container>
|
||||
</FloatingContainer>
|
||||
</LeftWrap>
|
||||
<CenterWrap>
|
||||
<Container>
|
||||
<FloatingContainer>
|
||||
<PrimaryButton
|
||||
label={ShapeType.Draw}
|
||||
onClick={selectDrawTool}
|
||||
|
@ -80,10 +81,10 @@ export default function ToolsPanel(): JSX.Element {
|
|||
>
|
||||
<TextIcon />
|
||||
</PrimaryButton>
|
||||
</Container>
|
||||
</FloatingContainer>
|
||||
</CenterWrap>
|
||||
<RightWrap size={{ '@initial': 'mobile', '@sm': 'small' }}>
|
||||
<Container>
|
||||
<FloatingContainer>
|
||||
<SecondaryButton
|
||||
label={'Lock Tool'}
|
||||
onClick={toggleToolLock}
|
||||
|
@ -91,7 +92,7 @@ export default function ToolsPanel(): JSX.Element {
|
|||
>
|
||||
{isToolLocked ? <LockClosedIcon /> : <LockOpen1Icon />}
|
||||
</SecondaryButton>
|
||||
</Container>
|
||||
</FloatingContainer>
|
||||
<UndoRedo />
|
||||
</RightWrap>
|
||||
</ToolsPanelContainer>
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { useCallback } from 'react'
|
||||
import state, { useSelector } from 'state'
|
||||
import { Theme } from 'types'
|
||||
|
||||
export default function useTheme() {
|
||||
const theme = useSelector((state) =>
|
||||
const theme: Theme = useSelector((state) =>
|
||||
state.data.settings.isDarkMode ? 'dark' : 'light'
|
||||
)
|
||||
|
||||
|
|
|
@ -7,60 +7,67 @@ export default function Sponsorware(): JSX.Element {
|
|||
const [session, loading] = useSession()
|
||||
|
||||
return (
|
||||
<Content
|
||||
size={{
|
||||
'@sm': 'small',
|
||||
}}
|
||||
>
|
||||
<h1>tldraw (is sponsorware)</h1>
|
||||
<p>
|
||||
Hey, thanks for visiting <a href="https://tldraw.com/">tldraw</a>, a
|
||||
tiny little drawing app by{' '}
|
||||
<a href="https://twitter.com/steveruizok">steveruizok</a>.
|
||||
</p>
|
||||
<video autoPlay muted playsInline onClick={(e) => e.currentTarget.play()}>
|
||||
<source src="images/hello.mp4" type="video/mp4" />
|
||||
</video>
|
||||
<p>This project is currently: </p>
|
||||
<ul>
|
||||
<li>in development</li>
|
||||
<li>only available for my sponsors</li>
|
||||
</ul>
|
||||
<p>
|
||||
If you'd like to try it out,{' '}
|
||||
<a
|
||||
href="https://github.com/sponsors/steveruizok"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
<OuterContent>
|
||||
<Content
|
||||
size={{
|
||||
'@sm': 'small',
|
||||
}}
|
||||
>
|
||||
<h1>tldraw (is sponsorware)</h1>
|
||||
<p>
|
||||
Hey, thanks for visiting <a href="https://tldraw.com/">tldraw</a>, a
|
||||
tiny little drawing app by{' '}
|
||||
<a href="https://twitter.com/steveruizok">steveruizok</a>.
|
||||
</p>
|
||||
<video
|
||||
autoPlay
|
||||
muted
|
||||
playsInline
|
||||
onClick={(e) => e.currentTarget.play()}
|
||||
>
|
||||
sponsor me on Github
|
||||
</a>{' '}
|
||||
(at any level) and sign in below.
|
||||
</p>
|
||||
<ButtonGroup>
|
||||
{session ? (
|
||||
<>
|
||||
<Button onClick={() => signout()} variant={'secondary'}>
|
||||
Sign Out
|
||||
</Button>
|
||||
<Detail>
|
||||
Signed in as {session?.user?.name} ({session?.user?.email}), but
|
||||
it looks like you're not yet a sponsor.
|
||||
<br />
|
||||
Something wrong? Try <a href="/">reloading the page</a> or DM me
|
||||
on <a href="https://twitter.com/steveruizok">Twitter</a>.
|
||||
</Detail>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button onClick={() => signin('github')} variant={'primary'}>
|
||||
{loading ? 'Loading...' : 'Sign in With Github'}
|
||||
</Button>
|
||||
<Detail>Already a sponsor? Just sign in to visit the app.</Detail>
|
||||
</>
|
||||
)}
|
||||
</ButtonGroup>
|
||||
</Content>
|
||||
<source src="images/hello.mp4" type="video/mp4" />
|
||||
</video>
|
||||
<p>This project is currently: </p>
|
||||
<ul>
|
||||
<li>in development</li>
|
||||
<li>only available for my sponsors</li>
|
||||
</ul>
|
||||
<p>
|
||||
If you'd like to try it out,{' '}
|
||||
<a
|
||||
href="https://github.com/sponsors/steveruizok"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
sponsor me on Github
|
||||
</a>{' '}
|
||||
(at any level) and sign in below.
|
||||
</p>
|
||||
<ButtonGroup>
|
||||
{session ? (
|
||||
<>
|
||||
<Button onClick={() => signout()} variant={'secondary'}>
|
||||
Sign Out
|
||||
</Button>
|
||||
<Detail>
|
||||
Signed in as {session?.user?.name} ({session?.user?.email}), but
|
||||
it looks like you're not yet a sponsor.
|
||||
<br />
|
||||
Something wrong? Try <a href="/">reloading the page</a> or DM me
|
||||
on <a href="https://twitter.com/steveruizok">Twitter</a>.
|
||||
</Detail>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button onClick={() => signin('github')} variant={'primary'}>
|
||||
{loading ? 'Loading...' : 'Sign in With Github'}
|
||||
</Button>
|
||||
<Detail>Already a sponsor? Just sign in to visit the app.</Detail>
|
||||
</>
|
||||
)}
|
||||
</ButtonGroup>
|
||||
</Content>
|
||||
</OuterContent>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -74,15 +81,29 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
|
|||
}
|
||||
}
|
||||
|
||||
const OuterContent = styled('div', {
|
||||
backgroundColor: '$canvas',
|
||||
padding: '32px',
|
||||
margin: '0 auto',
|
||||
overflow: 'scroll',
|
||||
position: 'fixed',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'flex-start',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
})
|
||||
|
||||
const Content = styled('div', {
|
||||
width: '720px',
|
||||
maxWidth: '100%',
|
||||
backgroundColor: '$panel',
|
||||
margin: '32px auto',
|
||||
borderRadius: '0px',
|
||||
boxShadow: '0px 2px 24px rgba(0,0,0,.08), 0px 2px 4px rgba(0,0,0,.16)',
|
||||
padding: '16px',
|
||||
overflow: 'hidden',
|
||||
borderRadius: '4px',
|
||||
boxShadow: '$12',
|
||||
color: '$text',
|
||||
fontSize: '$2',
|
||||
fontFamily: '$body',
|
||||
|
@ -154,7 +175,7 @@ const Button = styled('button', {
|
|||
fontWeight: 'bold',
|
||||
background: '$bounds',
|
||||
color: '$panel',
|
||||
boxShadow: '0px 2px 4px rgba(0,0,0,.2)',
|
||||
boxShadow: '$4',
|
||||
},
|
||||
secondary: {
|
||||
border: '1px solid $overlay',
|
||||
|
|
|
@ -1,33 +1,50 @@
|
|||
import { ColorStyle, DashStyle, ShapeStyles, SizeStyle } from 'types'
|
||||
import { Theme, ColorStyle, DashStyle, ShapeStyles, SizeStyle } from 'types'
|
||||
import { lerpColor } from 'utils'
|
||||
|
||||
export const strokes: Record<ColorStyle, string> = {
|
||||
[ColorStyle.White]: 'rgba(248, 249, 250, 1.000)',
|
||||
[ColorStyle.LightGray]: 'rgba(224, 226, 230, 1.000)',
|
||||
[ColorStyle.Gray]: 'rgba(172, 181, 189, 1.000)',
|
||||
[ColorStyle.Black]: 'rgba(0,0,0, 1.000)',
|
||||
[ColorStyle.Green]: 'rgba(54, 178, 77, 1.000)',
|
||||
[ColorStyle.Cyan]: 'rgba(14, 152, 173, 1.000)',
|
||||
[ColorStyle.Blue]: 'rgba(28, 126, 214, 1.000)',
|
||||
[ColorStyle.Indigo]: 'rgba(66, 99, 235, 1.000)',
|
||||
[ColorStyle.Violet]: 'rgba(112, 72, 232, 1.000)',
|
||||
[ColorStyle.Red]: 'rgba(240, 63, 63, 1.000)',
|
||||
[ColorStyle.Orange]: 'rgba(247, 103, 6, 1.000)',
|
||||
[ColorStyle.Yellow]: 'rgba(245, 159, 0, 1.000)',
|
||||
const canvasLight = '#fafafa'
|
||||
|
||||
const canvasDark = '#343d45'
|
||||
|
||||
const colors = {
|
||||
[ColorStyle.White]: '#f0f1f3',
|
||||
[ColorStyle.LightGray]: '#c6cbd1',
|
||||
[ColorStyle.Gray]: '#788492',
|
||||
[ColorStyle.Black]: '#212528',
|
||||
[ColorStyle.Green]: '#36b24d',
|
||||
[ColorStyle.Cyan]: '#0e98ad',
|
||||
[ColorStyle.Blue]: '#1c7ed6',
|
||||
[ColorStyle.Indigo]: '#4263eb',
|
||||
[ColorStyle.Violet]: '#7746f1',
|
||||
[ColorStyle.Red]: '#ff2133',
|
||||
[ColorStyle.Orange]: '#ff9433',
|
||||
[ColorStyle.Yellow]: '#ffc936',
|
||||
}
|
||||
|
||||
export const fills = {
|
||||
[ColorStyle.White]: 'rgba(224, 226, 230, 1.000)',
|
||||
[ColorStyle.LightGray]: 'rgba(255, 255, 255, 1.000)',
|
||||
[ColorStyle.Gray]: 'rgba(224, 226, 230, 1.000)',
|
||||
[ColorStyle.Black]: 'rgba(255, 255, 255, 1.000)',
|
||||
[ColorStyle.Green]: 'rgba(235, 251, 238, 1.000)',
|
||||
[ColorStyle.Cyan]: 'rgba(227, 250, 251, 1.000)',
|
||||
[ColorStyle.Blue]: 'rgba(231, 245, 255, 1.000)',
|
||||
[ColorStyle.Indigo]: 'rgba(237, 242, 255, 1.000)',
|
||||
[ColorStyle.Violet]: 'rgba(242, 240, 255, 1.000)',
|
||||
[ColorStyle.Red]: 'rgba(255, 245, 245, 1.000)',
|
||||
[ColorStyle.Orange]: 'rgba(255, 244, 229, 1.000)',
|
||||
[ColorStyle.Yellow]: 'rgba(255, 249, 219, 1.000)',
|
||||
export const strokes: Record<Theme, Record<ColorStyle, string>> = {
|
||||
light: colors,
|
||||
dark: {
|
||||
...(Object.fromEntries(
|
||||
Object.entries(colors).map(([k, v]) => [k, lerpColor(v, canvasDark, 0.1)])
|
||||
) as Record<ColorStyle, string>),
|
||||
[ColorStyle.White]: '#ffffff',
|
||||
[ColorStyle.Black]: '#000',
|
||||
},
|
||||
}
|
||||
|
||||
export const fills: Record<Theme, Record<ColorStyle, string>> = {
|
||||
light: {
|
||||
...(Object.fromEntries(
|
||||
Object.entries(colors).map(([k, v]) => [
|
||||
k,
|
||||
lerpColor(v, canvasLight, 0.82),
|
||||
])
|
||||
) as Record<ColorStyle, string>),
|
||||
[ColorStyle.White]: '#ffffff',
|
||||
[ColorStyle.Black]: '#ffffff',
|
||||
},
|
||||
dark: Object.fromEntries(
|
||||
Object.entries(colors).map(([k, v]) => [k, lerpColor(v, canvasDark, 0.618)])
|
||||
) as Record<ColorStyle, string>,
|
||||
}
|
||||
|
||||
const strokeWidths = {
|
||||
|
@ -57,7 +74,10 @@ export function getFontStyle(scale: number, style: ShapeStyles): string {
|
|||
return `${fontSize * scale}px/1.4 Verveine Regular`
|
||||
}
|
||||
|
||||
export function getShapeStyle(style: ShapeStyles): {
|
||||
export function getShapeStyle(
|
||||
style: ShapeStyles,
|
||||
isDarkMode = false
|
||||
): {
|
||||
stroke: string
|
||||
fill: string
|
||||
strokeWidth: number
|
||||
|
@ -66,9 +86,11 @@ export function getShapeStyle(style: ShapeStyles): {
|
|||
|
||||
const strokeWidth = getStrokeWidth(size)
|
||||
|
||||
const theme: Theme = isDarkMode ? 'dark' : 'light'
|
||||
|
||||
return {
|
||||
stroke: strokes[color],
|
||||
fill: isFilled ? fills[color] : 'none',
|
||||
stroke: strokes[theme][color],
|
||||
fill: isFilled ? fills[theme][color] : 'none',
|
||||
strokeWidth,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -101,7 +101,7 @@ const arrow = registerShapeUtils<ArrowShape>({
|
|||
return shape.handles !== prev.handles || shape.style !== prev.style
|
||||
},
|
||||
|
||||
render(shape) {
|
||||
render(shape, { isDarkMode }) {
|
||||
const { bend, handles, style } = shape
|
||||
const { start, end, bend: _bend } = handles
|
||||
|
||||
|
@ -110,7 +110,7 @@ const arrow = registerShapeUtils<ArrowShape>({
|
|||
|
||||
const isDraw = shape.style.dash === DashStyle.Draw
|
||||
|
||||
const styles = getShapeStyle(style)
|
||||
const styles = getShapeStyle(style, isDarkMode)
|
||||
|
||||
const { strokeWidth } = styles
|
||||
|
||||
|
|
|
@ -19,8 +19,8 @@ const dot = registerShapeUtils<DotShape>({
|
|||
style: defaultStyle,
|
||||
},
|
||||
|
||||
render(shape) {
|
||||
const styles = getShapeStyle(shape.style)
|
||||
render(shape, { isDarkMode }) {
|
||||
const styles = getShapeStyle(shape.style, isDarkMode)
|
||||
|
||||
return <use href="#dot" stroke={styles.stroke} fill={styles.stroke} />
|
||||
},
|
||||
|
|
|
@ -40,10 +40,10 @@ const draw = registerShapeUtils<DrawShape>({
|
|||
return shape.points !== prev.points || shape.style !== prev.style
|
||||
},
|
||||
|
||||
render(shape, { isHovered }) {
|
||||
render(shape, { isHovered, isDarkMode }) {
|
||||
const { points, style } = shape
|
||||
|
||||
const styles = getShapeStyle(style)
|
||||
const styles = getShapeStyle(style, isDarkMode)
|
||||
|
||||
const strokeWidth = +styles.strokeWidth
|
||||
|
||||
|
|
|
@ -42,9 +42,9 @@ const ellipse = registerShapeUtils<EllipseShape>({
|
|||
)
|
||||
},
|
||||
|
||||
render(shape) {
|
||||
render(shape, { isDarkMode }) {
|
||||
const { radiusX, radiusY, style } = shape
|
||||
const styles = getShapeStyle(style)
|
||||
const styles = getShapeStyle(style, isDarkMode)
|
||||
const strokeWidth = +styles.strokeWidth
|
||||
|
||||
const rx = Math.max(0, radiusX - strokeWidth / 2)
|
||||
|
|
|
@ -26,12 +26,12 @@ const line = registerShapeUtils<LineShape>({
|
|||
return shape.direction !== prev.direction || shape.style !== prev.style
|
||||
},
|
||||
|
||||
render(shape, { isHovered }) {
|
||||
render(shape, { isHovered, isDarkMode }) {
|
||||
const { id, direction } = shape
|
||||
const [x1, y1] = vec.add([0, 0], vec.mul(direction, 10000))
|
||||
const [x2, y2] = vec.sub([0, 0], vec.mul(direction, 10000))
|
||||
|
||||
const styles = getShapeStyle(shape.style)
|
||||
const styles = getShapeStyle(shape.style, isDarkMode)
|
||||
|
||||
return (
|
||||
<g id={id} filter={isHovered ? 'url(#expand)' : 'none'}>
|
||||
|
|
|
@ -28,10 +28,10 @@ const polyline = registerShapeUtils<PolylineShape>({
|
|||
shouldRender(shape, prev) {
|
||||
return shape.points !== prev.points || shape.style !== prev.style
|
||||
},
|
||||
render(shape) {
|
||||
render(shape, { isDarkMode }) {
|
||||
const { points, style } = shape
|
||||
|
||||
const styles = getShapeStyle(style)
|
||||
const styles = getShapeStyle(style, isDarkMode)
|
||||
|
||||
return (
|
||||
<polyline
|
||||
|
|
|
@ -25,10 +25,10 @@ const ray = registerShapeUtils<RayShape>({
|
|||
shouldRender(shape, prev) {
|
||||
return shape.direction !== prev.direction || shape.style !== prev.style
|
||||
},
|
||||
render(shape) {
|
||||
render(shape, { isDarkMode }) {
|
||||
const { direction } = shape
|
||||
|
||||
const styles = getShapeStyle(shape.style)
|
||||
const styles = getShapeStyle(shape.style, isDarkMode)
|
||||
|
||||
const [x2, y2] = vec.add([0, 0], vec.mul(direction, 10000))
|
||||
|
||||
|
|
|
@ -28,9 +28,9 @@ const rectangle = registerShapeUtils<RectangleShape>({
|
|||
return shape.size !== prev.size || shape.style !== prev.style
|
||||
},
|
||||
|
||||
render(shape, { isHovered }) {
|
||||
render(shape, { isHovered, isDarkMode }) {
|
||||
const { id, size, radius, style } = shape
|
||||
const styles = getShapeStyle(style)
|
||||
const styles = getShapeStyle(style, isDarkMode)
|
||||
const strokeWidth = +styles.strokeWidth
|
||||
|
||||
if (style.dash === DashStyle.Draw) {
|
||||
|
|
|
@ -70,9 +70,9 @@ const text = registerShapeUtils<TextShape>({
|
|||
)
|
||||
},
|
||||
|
||||
render(shape, { isEditing, ref }) {
|
||||
render(shape, { isEditing, isDarkMode, ref }) {
|
||||
const { id, text, style } = shape
|
||||
const styles = getShapeStyle(style)
|
||||
const styles = getShapeStyle(style, isDarkMode)
|
||||
const font = getFontStyle(shape.scale, shape.style)
|
||||
|
||||
const bounds = this.getBounds(shape)
|
||||
|
|
|
@ -8,6 +8,7 @@ import storage from './storage'
|
|||
import session from './session'
|
||||
import clipboard from './clipboard'
|
||||
import commands from './commands'
|
||||
import { dark, light } from 'styles'
|
||||
import {
|
||||
vec,
|
||||
getCommonBounds,
|
||||
|
@ -43,7 +44,7 @@ const initialData: Data = {
|
|||
settings: {
|
||||
fontSize: 13,
|
||||
isTestMode: false,
|
||||
isDarkMode: false,
|
||||
isDarkMode: true,
|
||||
isCodeOpen: false,
|
||||
isDebugMode: false,
|
||||
isDebugOpen: false,
|
||||
|
@ -144,6 +145,7 @@ for (let i = 0; i < count; i++) {
|
|||
|
||||
const state = createState({
|
||||
data: initialData,
|
||||
onEnter: 'applyTheme',
|
||||
on: {
|
||||
TOGGLED_DEBUG_PANEL: 'toggleDebugPanel',
|
||||
TOGGLED_DEBUG_MODE: 'toggleDebugMode',
|
||||
|
@ -168,12 +170,15 @@ const state = createState({
|
|||
},
|
||||
},
|
||||
ready: {
|
||||
onEnter: {
|
||||
wait: 0.01,
|
||||
if: 'hasSelection',
|
||||
do: 'zoomCameraToSelectionActual',
|
||||
else: ['zoomCameraToActual'],
|
||||
},
|
||||
onEnter: [
|
||||
'applyTheme',
|
||||
{
|
||||
wait: 0.01,
|
||||
if: 'hasSelection',
|
||||
do: 'zoomCameraToSelectionActual',
|
||||
else: ['zoomCameraToActual'],
|
||||
},
|
||||
],
|
||||
on: {
|
||||
UNMOUNTED: {
|
||||
do: ['saveDocumentState', 'resetDocumentState'],
|
||||
|
@ -203,6 +208,9 @@ const state = createState({
|
|||
unlessAny: ['isReadOnly', 'isInSession'],
|
||||
do: 'pasteShapesFromClipboard',
|
||||
},
|
||||
TOGGLED_DARK_MODE: {
|
||||
do: ['toggleDarkMode', 'applyTheme'],
|
||||
},
|
||||
TOGGLED_SHAPE_LOCK: {
|
||||
unlessAny: ['isReadOnly', 'isInSession'],
|
||||
if: 'hasSelection',
|
||||
|
@ -1263,6 +1271,7 @@ const state = createState({
|
|||
copyDebugLog() {
|
||||
logger.copyToJson()
|
||||
},
|
||||
|
||||
// Networked Room
|
||||
addRtShape(data, payload: { pageId: string; shape: Shape }) {
|
||||
const { pageId, shape } = payload
|
||||
|
@ -1962,6 +1971,21 @@ const state = createState({
|
|||
history.reset()
|
||||
},
|
||||
|
||||
/* ------------------- Preferences ------------------ */
|
||||
|
||||
toggleDarkMode(data) {
|
||||
data.settings.isDarkMode = !data.settings.isDarkMode
|
||||
},
|
||||
applyTheme(data) {
|
||||
if (data.settings.isDarkMode && typeof document !== 'undefined') {
|
||||
document.body.classList.remove(light)
|
||||
document.body.classList.add(dark)
|
||||
} else {
|
||||
document.body.classList.remove(dark)
|
||||
document.body.classList.add(light)
|
||||
}
|
||||
},
|
||||
|
||||
/* --------------------- Styles --------------------- */
|
||||
|
||||
toggleStylePanel(data) {
|
||||
|
|
|
@ -26,7 +26,20 @@ const { styled, global, css, theme, getCssString } = createCss({
|
|||
inputBorder: '#ddd',
|
||||
lineError: 'rgba(255, 0, 0, .1)',
|
||||
},
|
||||
space: {},
|
||||
shadows: {
|
||||
2: '0px 1px 1px rgba(0, 0, 0, 0.14)',
|
||||
3: '0px 2px 3px rgba(0, 0, 0, 0.14)',
|
||||
4: '0px 4px 5px -1px rgba(0, 0, 0, 0.14)',
|
||||
8: '0px 12px 17px rgba(0, 0, 0, 0.14)',
|
||||
12: '0px 12px 17px rgba(0, 0, 0, 0.14)',
|
||||
24: '0px 24px 38px rgba(0, 0, 0, 0.14)',
|
||||
},
|
||||
space: {
|
||||
0: '2px',
|
||||
1: '3px',
|
||||
2: '4px',
|
||||
3: '8px',
|
||||
},
|
||||
fontSizes: {
|
||||
0: '10px',
|
||||
1: '12px',
|
||||
|
@ -43,10 +56,15 @@ const { styled, global, css, theme, getCssString } = createCss({
|
|||
lineHeights: {},
|
||||
letterSpacings: {},
|
||||
sizes: {},
|
||||
borderWidths: {},
|
||||
borderWidths: {
|
||||
0: '$1',
|
||||
},
|
||||
borderStyles: {},
|
||||
radii: {},
|
||||
shadows: {},
|
||||
radii: {
|
||||
0: '2px',
|
||||
1: '4px',
|
||||
2: '8px',
|
||||
},
|
||||
zIndices: {},
|
||||
transitions: {},
|
||||
},
|
||||
|
@ -76,7 +94,37 @@ const { styled, global, css, theme, getCssString } = createCss({
|
|||
|
||||
const light = theme({})
|
||||
|
||||
const dark = theme({})
|
||||
const dark = theme({
|
||||
colors: {
|
||||
brushFill: 'rgba(0,0,0,.05)',
|
||||
brushStroke: 'rgba(0,0,0,.25)',
|
||||
hint: 'rgba(216, 226, 249, 1.000)',
|
||||
selected: 'rgba(82, 143, 245, 1.000)',
|
||||
bounds: 'rgba(82, 143, 245, 1.000)',
|
||||
boundsBg: 'rgba(82, 143, 245, 0.05)',
|
||||
highlight: 'rgba(82, 143, 245, 0.15)',
|
||||
overlay: 'rgba(0, 0, 0, 0.15)',
|
||||
border: '#202529',
|
||||
canvas: '#343d45',
|
||||
panel: '#49555f',
|
||||
inactive: '#cccccf',
|
||||
hover: '#343d45',
|
||||
text: '#f8f9fa',
|
||||
muted: '#e0e2e6',
|
||||
input: '#f3f3f3',
|
||||
inputBorder: '#ddd',
|
||||
codeHl: 'rgba(144, 144, 144, .15)',
|
||||
lineError: 'rgba(255, 0, 0, .1)',
|
||||
},
|
||||
shadows: {
|
||||
2: '0px 1px 1px rgba(0, 0, 0, 0.24)',
|
||||
3: '0px 2px 3px rgba(0, 0, 0, 0.24)',
|
||||
4: '0px 4px 5px -1px rgba(0, 0, 0, 0.24)',
|
||||
8: '0px 12px 17px rgba(0, 0, 0, 0.24)',
|
||||
12: '0px 12px 17px rgba(0, 0, 0, 0.24)',
|
||||
24: '0px 24px 38px rgba(0, 0, 0, 0.24)',
|
||||
},
|
||||
})
|
||||
|
||||
const globalStyles = global({
|
||||
'*': { boxSizing: 'border-box' },
|
||||
|
@ -90,7 +138,12 @@ const globalStyles = global({
|
|||
overscrollBehavior: 'none',
|
||||
fontFamily: '$ui',
|
||||
fontSize: '$2',
|
||||
color: '$text',
|
||||
backgroundColor: '$canvas',
|
||||
minHeight: '100vh',
|
||||
},
|
||||
body: {
|
||||
overflow: 'hidden',
|
||||
},
|
||||
})
|
||||
|
||||
|
|
3
types.ts
3
types.ts
|
@ -124,6 +124,8 @@ export enum FontSize {
|
|||
ExtraLarge = 'ExtraLarge',
|
||||
}
|
||||
|
||||
export type Theme = 'dark' | 'light'
|
||||
|
||||
export type ShapeStyles = {
|
||||
color: ColorStyle
|
||||
size: SizeStyle
|
||||
|
@ -612,6 +614,7 @@ export interface ShapeUtility<K extends Shape> {
|
|||
isHovered?: boolean
|
||||
isSelected?: boolean
|
||||
isCurrentParent?: boolean
|
||||
isDarkMode?: boolean
|
||||
ref?: React.MutableRefObject<HTMLTextAreaElement>
|
||||
}
|
||||
): JSX.Element
|
||||
|
|
|
@ -20,6 +20,47 @@ export function lerp(y1: number, y2: number, mu: number): number {
|
|||
return y1 * (1 - mu) + y2 * mu
|
||||
}
|
||||
|
||||
/**
|
||||
* Linear interpolation between two colors.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
*```ts
|
||||
* lerpColor("#000000", "#0099FF", .25)
|
||||
*```
|
||||
*/
|
||||
function h2r(hex: string) {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
||||
return result
|
||||
? [
|
||||
parseInt(result[1], 16),
|
||||
parseInt(result[2], 16),
|
||||
parseInt(result[3], 16),
|
||||
]
|
||||
: null
|
||||
}
|
||||
|
||||
function r2h(rgb: number[]) {
|
||||
return (
|
||||
'#' +
|
||||
((1 << 24) + (rgb[0] << 16) + (rgb[1] << 8) + rgb[2]).toString(16).slice(1)
|
||||
)
|
||||
}
|
||||
|
||||
export function lerpColor(
|
||||
color1: string,
|
||||
color2: string,
|
||||
factor = 0.5
|
||||
): string {
|
||||
const c1 = h2r(color1)
|
||||
const c2 = h2r(color2)
|
||||
const result = c1.slice()
|
||||
for (let i = 0; i < 3; i++) {
|
||||
result[i] = Math.round(result[i] + factor * (c2[i] - c1[i]))
|
||||
}
|
||||
return r2h(result)
|
||||
}
|
||||
|
||||
/**
|
||||
* Modulate a value between two ranges.
|
||||
* @param value
|
||||
|
|
Loading…
Reference in a new issue