Adds dark mode

This commit is contained in:
Steve Ruiz 2021-07-10 21:39:29 +01:00
parent 8807aaf1b9
commit 1965c97f68
27 changed files with 428 additions and 265 deletions

View file

@ -52,6 +52,6 @@ const StyledCorner = styled('rect', {
const StyledCornerInner = styled('rect', {
stroke: '$bounds',
fill: '#fff',
fill: '$panel',
zStrokeWidth: 1.5,
})

View file

@ -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
})

View file

@ -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),
}

View file

@ -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,
})
}

View 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

View file

@ -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'

View file

@ -11,8 +11,9 @@ import {
DropdownMenuButton,
DropdownMenuSubMenu,
DropdownMenuDivider,
DropdownMenuCheckboxItem,
} from '../shared'
import state from 'state'
import state, { useSelector } from 'state'
import { commandKey } from 'utils'
const handleNew = () => state.send('CREATED_NEW_PROJECT')
@ -28,7 +29,7 @@ function Menu() {
<HamburgerMenuIcon />
</IconButton>
<Content as={MenuContent} sideOffset={8}>
<DropdownMenuButton onSelect={handleNew}>
<DropdownMenuButton onSelect={handleNew} disabled>
<span>New Project</span>
<kbd>
<span>{commandKey()}</span>
@ -72,7 +73,7 @@ export default memo(Menu)
function RecentFiles() {
return (
<DropdownMenuSubMenu label="Open Recent...">
<DropdownMenuSubMenu label="Open Recent..." disabled>
<DropdownMenuButton>
<span>Project A</span>
</DropdownMenuButton>
@ -87,16 +88,16 @@ function RecentFiles() {
}
function Preferences() {
const isDarkMode = useSelector((s) => s.data.settings.isDarkMode)
return (
<DropdownMenuSubMenu label="Preferences">
<DropdownMenuButton onSelect={toggleDarkMode}>
<span>Toggle Dark Mode</span>
<kbd>
<span></span>
<span>{commandKey()}</span>
<span>D</span>
</kbd>
</DropdownMenuButton>
<DropdownMenuCheckboxItem
checked={isDarkMode}
onCheckedChange={toggleDarkMode}
>
<span>Dark Mode</span>
</DropdownMenuCheckboxItem>
</DropdownMenuSubMenu>
)
}

View file

@ -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,152 +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', {
zIndex: 200,
overflow: 'hidden',
position: 'relative',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
pointerEvents: 'all',
padding: '$0',
borderRadius: '4px',
backgroundColor: '$panel',
border: '1px solid $panel',
boxShadow: '$4',
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: '$0',
borderRadius: '4px',
backgroundColor: '$panel',
border: '1px solid $panel',
boxShadow: '$4',
})
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,
},
})

File diff suppressed because one or more lines are too long

View file

@ -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',

View file

@ -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>

View file

@ -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 />

View file

@ -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'
)

View file

@ -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,
}
}

View file

@ -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

View file

@ -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} />
},

View file

@ -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

View file

@ -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)

View file

@ -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'}>

View file

@ -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

View file

@ -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))

View file

@ -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) {

View file

@ -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)

View file

@ -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'],
@ -204,7 +209,7 @@ const state = createState({
do: 'pasteShapesFromClipboard',
},
TOGGLED_DARK_MODE: {
do: 'toggleDarkMode',
do: ['toggleDarkMode', 'applyTheme'],
},
TOGGLED_SHAPE_LOCK: {
unlessAny: ['isReadOnly', 'isInSession'],
@ -1971,6 +1976,15 @@ const state = createState({
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 --------------------- */

View file

@ -96,26 +96,34 @@ const light = theme({})
const dark = theme({
colors: {
codeHl: 'rgba(144, 144, 144, .15)',
brushFill: 'rgba(0,0,0,.05)',
brushStroke: 'rgba(0,0,0,.25)',
hint: 'rgba(216, 226, 249, 1.000)',
selected: 'rgba(66, 133, 244, 1.000)',
bounds: 'rgba(65, 132, 244, 1.000)',
boundsBg: 'rgba(65, 132, 244, 0.05)',
highlight: 'rgba(65, 132, 244, 0.15)',
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: '#aaa',
canvas: '#fafafa',
panel: '#fefefe',
border: '#202529',
canvas: '#343d45',
panel: '#49555f',
inactive: '#cccccf',
hover: '#efefef',
text: '#333',
muted: '#777',
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({
@ -130,6 +138,7 @@ const globalStyles = global({
overscrollBehavior: 'none',
fontFamily: '$ui',
fontSize: '$2',
color: '$text',
backgroundColor: '$canvas',
},
body: {

View file

@ -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

View file

@ -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