diff --git a/components/canvas/bounds/corner-handle.tsx b/components/canvas/bounds/corner-handle.tsx index 55ffa3b3c..50c23438a 100644 --- a/components/canvas/bounds/corner-handle.tsx +++ b/components/canvas/bounds/corner-handle.tsx @@ -52,6 +52,6 @@ const StyledCorner = styled('rect', { const StyledCornerInner = styled('rect', { stroke: '$bounds', - fill: '#fff', + fill: '$panel', zStrokeWidth: 1.5, }) diff --git a/components/canvas/hovered-shape.tsx b/components/canvas/hovered-shape.tsx index cb05e9250..08969d8a3 100644 --- a/components/canvas/hovered-shape.tsx +++ b/components/canvas/hovered-shape.tsx @@ -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 }) diff --git a/components/canvas/page.tsx b/components/canvas/page.tsx index b7899da99..02ec3bde6 100644 --- a/components/canvas/page.tsx +++ b/components/canvas/page.tsx @@ -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), } diff --git a/components/canvas/shape.tsx b/components/canvas/shape.tsx index d2c5845f8..b70845771 100644 --- a/components/canvas/shape.tsx +++ b/components/canvas/shape.tsx @@ -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(null) @@ -39,13 +41,14 @@ const Shape = memo( {...events} > {isEditing && shape.type === ShapeType.Text ? ( - + ) : ( )} @@ -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(null) return getShapeUtils(shape).render(shape, { @@ -106,6 +119,7 @@ function EditingTextShape({ shape }: { shape: TextShape }) { isEditing: true, isHovered: false, isSelected: false, + isDarkMode, isCurrentParent: false, }) } diff --git a/components/icons/check.tsx b/components/icons/check.tsx new file mode 100644 index 000000000..a70df8b03 --- /dev/null +++ b/components/icons/check.tsx @@ -0,0 +1,25 @@ +import * as React from 'react' + +function SvgCheck(props: React.SVGProps): JSX.Element { + return ( + + + + + ) +} + +export default SvgCheck diff --git a/components/icons/index.tsx b/components/icons/index.tsx index 4ce8d6300..c3f81e636 100644 --- a/components/icons/index.tsx +++ b/components/icons/index.tsx @@ -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' diff --git a/components/menu/menu.tsx b/components/menu/menu.tsx index 9a57afbd4..09cd81e44 100644 --- a/components/menu/menu.tsx +++ b/components/menu/menu.tsx @@ -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() { - + New Project {commandKey()} @@ -72,7 +73,7 @@ export default memo(Menu) function RecentFiles() { return ( - + Project A @@ -87,16 +88,16 @@ function RecentFiles() { } function Preferences() { + const isDarkMode = useSelector((s) => s.data.settings.isDarkMode) + return ( - - Toggle Dark Mode - - - {commandKey()} - D - - + + Dark Mode + ) } diff --git a/components/page-panel/page-panel.tsx b/components/page-panel/page-panel.tsx index 361615680..664a7db7f 100644 --- a/components/page-panel/page-panel.tsx +++ b/components/page-panel/page-panel.tsx @@ -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 { } }} > - - + + {documentPages[currentPageId].name} - - - - { - setIsOpen(false) - state.send('CHANGED_PAGE', { id }) - }} - > - {sorted.map(({ id, name }) => ( - - - - {name} - - - - - - - - state.send('RENAMED_PAGE', { id })} - > - Rename - - { - setIsOpen(false) - state.send('DELETED_PAGE', { id }) - }} - > - Delete - - - - - ))} - - - { - setIsOpen(false) - state.send('CREATED_PAGE') - }} - > - Create Page - - - - - - - + + + + { + setIsOpen(false) + state.send('CHANGED_PAGE', { id }) + }} + > + {sorted.map(({ id, name }) => ( + + + {name} + + + + + + + + + + + ))} + + + state.send('CREATED_PAGE')}> + Create Page + + + + + ) } -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, }, }) diff --git a/components/shared.tsx b/components/shared.tsx index cd19c14be..7e1e1135f 100644 --- a/components/shared.tsx +++ b/components/shared.tsx @@ -4,7 +4,7 @@ import * as RadioGroup from '@radix-ui/react-radio-group' import * as Panel from './panel' import styled from 'styles' import { forwardRef } from 'react' -import { ChevronRightIcon } from '@radix-ui/react-icons' +import { CheckIcon, ChevronRightIcon } from '@radix-ui/react-icons' import { isMobile } from 'utils' export const breakpoints: any = { '@initial': 'mobile', '@sm': 'small' } @@ -24,6 +24,7 @@ export const IconButton = styled('button', { border: 'none', pointerEvents: 'all', fontSize: '$0', + color: '$text', cursor: 'pointer', '& > *': { @@ -44,7 +45,9 @@ export const IconButton = styled('button', { variants: { bp: { - mobile: {}, + mobile: { + backgroundColor: 'transparent', + }, small: { '&:hover:not(:disabled)': { backgroundColor: '$hover', @@ -93,6 +96,7 @@ export const RowButton = styled('button', { height: '32px', border: 'none', cursor: 'pointer', + color: '$text', outline: 'none', alignItems: 'center', fontFamily: '$ui', @@ -115,16 +119,22 @@ export const RowButton = styled('button', { zIndex: 1, }, - '& :disabled': { - opacity: 0.5, + '&:disabled': { + opacity: 0.1, }, variants: { bp: { mobile: {}, small: { + '& *[data-shy="true"]': { + opacity: 0, + }, '&:hover:not(:disabled)': { backgroundColor: '$hover', + '& *[data-shy="true"]': { + opacity: 1, + }, }, }, }, @@ -136,7 +146,31 @@ export const RowButton = styled('button', { }, variant: { pageButton: { - paddingRight: 12, + display: 'grid', + gridTemplateColumns: '24px auto', + width: '100%', + paddingLeft: '$1', + gap: '$3', + justifyContent: 'flex-start', + [`& > *[data-state="checked"]`]: { + gridRow: 1, + gridColumn: 1, + }, + '& > span': { + gridRow: 1, + gridColumn: 2, + width: '100%', + }, + }, + }, + disabled: { + true: { + opacity: 0.3, + }, + }, + isActive: { + true: { + backgroundColor: '$hover', }, }, }, @@ -224,6 +258,7 @@ export const IconWrapper = styled('div', { border: 'none', pointerEvents: 'all', cursor: 'pointer', + color: '$text', '& svg': { height: 22, @@ -356,7 +391,7 @@ export function DashDrawIcon(): JSX.Element { export function BoxIcon({ fill = 'none', - stroke = 'black', + stroke = 'currentColor', }: { fill?: string stroke?: string @@ -383,14 +418,12 @@ export const IsFilledFillIcon = forwardRef( width="24" height="24" viewBox="0 0 24 24" - fill="none" + fill="currentColor" stroke="currentColor" xmlns="http://www.w3.org/2000/svg" > @@ -496,28 +529,34 @@ export function DropdownMenuRoot({ export function DropdownMenuSubMenu({ children, + disabled = false, label, }: { label: string + disabled?: boolean children: React.ReactNode }): JSX.Element { return ( - + {label} - - + {children} - + ) } @@ -525,7 +564,10 @@ export function DropdownMenuSubMenu({ export const DropdownMenuDivider = styled(DropdownMenu.Separator, { backgroundColor: '$hover', height: 1, - margin: '$2 -$2', + marginTop: '$2', + marginRight: '-$2', + marginBottom: '$2', + marginLeft: '-$2', }) export const DropdownMenuArrow = styled(DropdownMenu.Arrow, { @@ -542,35 +584,68 @@ export function DropdownMenuButton({ children: React.ReactNode }): JSX.Element { return ( - {children} - + ) } +interface DropdownMenuIconButtonProps { + onSelect: () => void + disabled?: boolean + children: React.ReactNode +} + export function DropdownMenuIconButton({ onSelect, children, disabled = false, -}: { - onSelect: () => void - disabled?: boolean - children: React.ReactNode -}): JSX.Element { +}: DropdownMenuIconButtonProps): JSX.Element { return ( - {children} - + + ) +} + +interface MenuCheckboxItemProps { + checked: boolean + disabled?: boolean + onCheckedChange: (isChecked: boolean) => void + children: React.ReactNode +} + +export function DropdownMenuCheckboxItem({ + checked, + disabled = false, + onCheckedChange, + children, +}: MenuCheckboxItemProps): JSX.Element { + return ( + + {children} + + + + + + ) } @@ -640,14 +715,14 @@ export function ContextMenuButton({ children: React.ReactNode }): JSX.Element { return ( - {children} - + ) } @@ -671,3 +746,27 @@ export function ContextMenuIconButton({ ) } + +export function ContextMenuCheckboxItem({ + checked, + disabled = false, + onCheckedChange, + children, +}: MenuCheckboxItemProps): JSX.Element { + return ( + + {children} + + + + + + + ) +} diff --git a/components/status-bar.tsx b/components/status-bar.tsx index c47d3fe21..86faf018c 100644 --- a/components/status-bar.tsx +++ b/components/status-bar.tsx @@ -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', diff --git a/components/style-panel/color-content.tsx b/components/style-panel/color-content.tsx index 0e34fb1b6..2f64e1765 100644 --- a/components/style-panel/color-content.tsx +++ b/components/style-panel/color-content.tsx @@ -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 ( - {Object.keys(strokes).map((color: ColorStyle) => ( + {Object.keys(strokes[theme]).map((color: ColorStyle) => ( - + ))} diff --git a/components/style-panel/quick-color-select.tsx b/components/style-panel/quick-color-select.tsx index 6ab7b79c6..015baa1d1 100644 --- a/components/style-panel/quick-color-select.tsx +++ b/components/style-panel/quick-color-select.tsx @@ -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 ( - + diff --git a/hooks/useTheme.ts b/hooks/useTheme.ts index b738b909e..a157f4290 100644 --- a/hooks/useTheme.ts +++ b/hooks/useTheme.ts @@ -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' ) diff --git a/state/shape-styles.ts b/state/shape-styles.ts index b55af506d..3fb33a45c 100644 --- a/state/shape-styles.ts +++ b/state/shape-styles.ts @@ -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.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> = { + light: colors, + dark: { + ...(Object.fromEntries( + Object.entries(colors).map(([k, v]) => [k, lerpColor(v, canvasDark, 0.1)]) + ) as Record), + [ColorStyle.White]: '#ffffff', + [ColorStyle.Black]: '#000', + }, +} + +export const fills: Record> = { + light: { + ...(Object.fromEntries( + Object.entries(colors).map(([k, v]) => [ + k, + lerpColor(v, canvasLight, 0.82), + ]) + ) as Record), + [ColorStyle.White]: '#ffffff', + [ColorStyle.Black]: '#ffffff', + }, + dark: Object.fromEntries( + Object.entries(colors).map(([k, v]) => [k, lerpColor(v, canvasDark, 0.618)]) + ) as Record, } 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, } } diff --git a/state/shape-utils/arrow.tsx b/state/shape-utils/arrow.tsx index 70375812a..b72b56eca 100644 --- a/state/shape-utils/arrow.tsx +++ b/state/shape-utils/arrow.tsx @@ -101,7 +101,7 @@ const arrow = registerShapeUtils({ 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({ const isDraw = shape.style.dash === DashStyle.Draw - const styles = getShapeStyle(style) + const styles = getShapeStyle(style, isDarkMode) const { strokeWidth } = styles diff --git a/state/shape-utils/dot.tsx b/state/shape-utils/dot.tsx index 302f9d960..0a49efac1 100644 --- a/state/shape-utils/dot.tsx +++ b/state/shape-utils/dot.tsx @@ -19,8 +19,8 @@ const dot = registerShapeUtils({ style: defaultStyle, }, - render(shape) { - const styles = getShapeStyle(shape.style) + render(shape, { isDarkMode }) { + const styles = getShapeStyle(shape.style, isDarkMode) return }, diff --git a/state/shape-utils/draw.tsx b/state/shape-utils/draw.tsx index 9dc5f240a..83ebf7967 100644 --- a/state/shape-utils/draw.tsx +++ b/state/shape-utils/draw.tsx @@ -40,10 +40,10 @@ const draw = registerShapeUtils({ 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 diff --git a/state/shape-utils/ellipse.tsx b/state/shape-utils/ellipse.tsx index 062bef8c0..31f702a9c 100644 --- a/state/shape-utils/ellipse.tsx +++ b/state/shape-utils/ellipse.tsx @@ -42,9 +42,9 @@ const ellipse = registerShapeUtils({ ) }, - 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) diff --git a/state/shape-utils/line.tsx b/state/shape-utils/line.tsx index d904089a6..1e5167a62 100644 --- a/state/shape-utils/line.tsx +++ b/state/shape-utils/line.tsx @@ -26,12 +26,12 @@ const line = registerShapeUtils({ 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 ( diff --git a/state/shape-utils/polyline.tsx b/state/shape-utils/polyline.tsx index 4571f57e0..7b9b38855 100644 --- a/state/shape-utils/polyline.tsx +++ b/state/shape-utils/polyline.tsx @@ -28,10 +28,10 @@ const polyline = registerShapeUtils({ 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 ( ({ 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)) diff --git a/state/shape-utils/rectangle.tsx b/state/shape-utils/rectangle.tsx index 31ef55c10..3bdcc0389 100644 --- a/state/shape-utils/rectangle.tsx +++ b/state/shape-utils/rectangle.tsx @@ -28,9 +28,9 @@ const rectangle = registerShapeUtils({ 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) { diff --git a/state/shape-utils/text.tsx b/state/shape-utils/text.tsx index 676217e2f..8fa18069a 100644 --- a/state/shape-utils/text.tsx +++ b/state/shape-utils/text.tsx @@ -70,9 +70,9 @@ const text = registerShapeUtils({ ) }, - 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) diff --git a/state/state.ts b/state/state.ts index 0f452c3fa..851645cbc 100644 --- a/state/state.ts +++ b/state/state.ts @@ -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 --------------------- */ diff --git a/styles/stitches.config.ts b/styles/stitches.config.ts index d4e33bf43..b5d5c15d6 100644 --- a/styles/stitches.config.ts +++ b/styles/stitches.config.ts @@ -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: { diff --git a/types.ts b/types.ts index c49e93f81..43fee715d 100644 --- a/types.ts +++ b/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 { isHovered?: boolean isSelected?: boolean isCurrentParent?: boolean + isDarkMode?: boolean ref?: React.MutableRefObject } ): JSX.Element diff --git a/utils/utils.ts b/utils/utils.ts index 622989b36..a647533f7 100644 --- a/utils/utils.ts +++ b/utils/utils.ts @@ -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