浏览代码

Adds dark mode

main
Steve Ruiz 4 年前
父节点
当前提交
1965c97f68

+ 1
- 1
components/canvas/bounds/corner-handle.tsx 查看文件

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

+ 1
- 1
components/canvas/hovered-shape.tsx 查看文件

@@ -22,7 +22,7 @@ function HoveredShape({ id }: { id: string }) {
22 22
 
23 23
   const strokeWidth = useSelector((s) => {
24 24
     const shape = tld.getShape(s.data, id)
25
-    const style = getShapeStyle(shape.style)
25
+    const style = getShapeStyle(shape.style, s.data.settings.isDarkMode)
26 26
     return +style.strokeWidth
27 27
   })
28 28
 

+ 12
- 1
components/canvas/page.tsx 查看文件

@@ -16,6 +16,7 @@ interface Node {
16 16
   isEditing: boolean
17 17
   isHovered: boolean
18 18
   isSelected: boolean
19
+  isDarkMode: boolean
19 20
   isCurrentParent: boolean
20 21
 }
21 22
 
@@ -65,7 +66,15 @@ interface ShapeNodeProps {
65 66
 }
66 67
 
67 68
 const ShapeNode = ({
68
-  node: { shape, children, isEditing, isHovered, isSelected, isCurrentParent },
69
+  node: {
70
+    shape,
71
+    children,
72
+    isEditing,
73
+    isHovered,
74
+    isDarkMode,
75
+    isSelected,
76
+    isCurrentParent,
77
+  },
69 78
 }: ShapeNodeProps) => {
70 79
   return (
71 80
     <>
@@ -74,6 +83,7 @@ const ShapeNode = ({
74 83
         isEditing={isEditing}
75 84
         isHovered={isHovered}
76 85
         isSelected={isSelected}
86
+        isDarkMode={isDarkMode}
77 87
         isCurrentParent={isCurrentParent}
78 88
       />
79 89
       {children.map((childNode) => (
@@ -105,6 +115,7 @@ function addToTree(
105 115
     isHovered: data.hoveredId === shape.id,
106 116
     isCurrentParent: data.currentParentId === shape.id,
107 117
     isEditing: data.editingId === shape.id,
118
+    isDarkMode: data.settings.isDarkMode,
108 119
     isSelected: selectedIds.includes(shape.id),
109 120
   }
110 121
 

+ 16
- 2
components/canvas/shape.tsx 查看文件

@@ -10,6 +10,7 @@ interface ShapeProps {
10 10
   isEditing: boolean
11 11
   isHovered: boolean
12 12
   isSelected: boolean
13
+  isDarkMode: boolean
13 14
   isCurrentParent: boolean
14 15
 }
15 16
 
@@ -19,6 +20,7 @@ const Shape = memo(
19 20
     isEditing,
20 21
     isHovered,
21 22
     isSelected,
23
+    isDarkMode,
22 24
     isCurrentParent,
23 25
   }: ShapeProps) => {
24 26
     const rGroup = useRef<SVGGElement>(null)
@@ -39,13 +41,14 @@ const Shape = memo(
39 41
         {...events}
40 42
       >
41 43
         {isEditing && shape.type === ShapeType.Text ? (
42
-          <EditingTextShape shape={shape} />
44
+          <EditingTextShape shape={shape} isDarkMode={isDarkMode} />
43 45
         ) : (
44 46
           <RenderedShape
45 47
             shape={shape}
46 48
             isEditing={isEditing}
47 49
             isHovered={isHovered}
48 50
             isSelected={isSelected}
51
+            isDarkMode={isDarkMode}
49 52
             isCurrentParent={isCurrentParent}
50 53
           />
51 54
         )}
@@ -62,6 +65,7 @@ interface RenderedShapeProps {
62 65
   isEditing: boolean
63 66
   isHovered: boolean
64 67
   isSelected: boolean
68
+  isDarkMode: boolean
65 69
   isCurrentParent: boolean
66 70
 }
67 71
 
@@ -71,12 +75,14 @@ const RenderedShape = memo(
71 75
     isEditing,
72 76
     isHovered,
73 77
     isSelected,
78
+    isDarkMode,
74 79
     isCurrentParent,
75 80
   }: RenderedShapeProps) {
76 81
     return getShapeUtils(shape).render(shape, {
77 82
       isEditing,
78 83
       isHovered,
79 84
       isSelected,
85
+      isDarkMode,
80 86
       isCurrentParent,
81 87
     })
82 88
   },
@@ -85,6 +91,7 @@ const RenderedShape = memo(
85 91
       prev.isEditing !== next.isEditing ||
86 92
       prev.isHovered !== next.isHovered ||
87 93
       prev.isSelected !== next.isSelected ||
94
+      prev.isDarkMode !== next.isDarkMode ||
88 95
       prev.isCurrentParent !== next.isCurrentParent
89 96
     ) {
90 97
       return false
@@ -98,7 +105,13 @@ const RenderedShape = memo(
98 105
   }
99 106
 )
100 107
 
101
-function EditingTextShape({ shape }: { shape: TextShape }) {
108
+function EditingTextShape({
109
+  shape,
110
+  isDarkMode,
111
+}: {
112
+  shape: TextShape
113
+  isDarkMode: boolean
114
+}) {
102 115
   const ref = useRef<HTMLTextAreaElement>(null)
103 116
 
104 117
   return getShapeUtils(shape).render(shape, {
@@ -106,6 +119,7 @@ function EditingTextShape({ shape }: { shape: TextShape }) {
106 119
     isEditing: true,
107 120
     isHovered: false,
108 121
     isSelected: false,
122
+    isDarkMode,
109 123
     isCurrentParent: false,
110 124
   })
111 125
 }

+ 25
- 0
components/icons/check.tsx 查看文件

@@ -0,0 +1,25 @@
1
+import * as React from 'react'
2
+
3
+function SvgCheck(props: React.SVGProps<SVGSVGElement>): JSX.Element {
4
+  return (
5
+    <svg
6
+      viewBox="0 0 15 15"
7
+      fill="currentColor"
8
+      xmlns="http://www.w3.org/2000/svg"
9
+      {...props}
10
+    >
11
+      <path
12
+        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"
13
+        fill="currentColor"
14
+      />
15
+      <path
16
+        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"
17
+        stroke="currentColor"
18
+        strokeLinecap="round"
19
+        strokeLinejoin="round"
20
+      />
21
+    </svg>
22
+  )
23
+}
24
+
25
+export default SvgCheck

+ 1
- 0
components/icons/index.tsx 查看文件

@@ -1,3 +1,4 @@
1 1
 export { default as Redo } from './redo'
2 2
 export { default as Trash } from './trash'
3 3
 export { default as Undo } from './undo'
4
+export { default as Check } from './check'

+ 12
- 11
components/menu/menu.tsx 查看文件

@@ -11,8 +11,9 @@ import {
11 11
   DropdownMenuButton,
12 12
   DropdownMenuSubMenu,
13 13
   DropdownMenuDivider,
14
+  DropdownMenuCheckboxItem,
14 15
 } from '../shared'
15
-import state from 'state'
16
+import state, { useSelector } from 'state'
16 17
 import { commandKey } from 'utils'
17 18
 
18 19
 const handleNew = () => state.send('CREATED_NEW_PROJECT')
@@ -28,7 +29,7 @@ function Menu() {
28 29
           <HamburgerMenuIcon />
29 30
         </IconButton>
30 31
         <Content as={MenuContent} sideOffset={8}>
31
-          <DropdownMenuButton onSelect={handleNew}>
32
+          <DropdownMenuButton onSelect={handleNew} disabled>
32 33
             <span>New Project</span>
33 34
             <kbd>
34 35
               <span>{commandKey()}</span>
@@ -72,7 +73,7 @@ export default memo(Menu)
72 73
 
73 74
 function RecentFiles() {
74 75
   return (
75
-    <DropdownMenuSubMenu label="Open Recent...">
76
+    <DropdownMenuSubMenu label="Open Recent..." disabled>
76 77
       <DropdownMenuButton>
77 78
         <span>Project A</span>
78 79
       </DropdownMenuButton>
@@ -87,16 +88,16 @@ function RecentFiles() {
87 88
 }
88 89
 
89 90
 function Preferences() {
91
+  const isDarkMode = useSelector((s) => s.data.settings.isDarkMode)
92
+
90 93
   return (
91 94
     <DropdownMenuSubMenu label="Preferences">
92
-      <DropdownMenuButton onSelect={toggleDarkMode}>
93
-        <span>Toggle Dark Mode</span>
94
-        <kbd>
95
-          <span>⇧</span>
96
-          <span>{commandKey()}</span>
97
-          <span>D</span>
98
-        </kbd>
99
-      </DropdownMenuButton>
95
+      <DropdownMenuCheckboxItem
96
+        checked={isDarkMode}
97
+        onCheckedChange={toggleDarkMode}
98
+      >
99
+        <span>Dark Mode</span>
100
+      </DropdownMenuCheckboxItem>
100 101
     </DropdownMenuSubMenu>
101 102
   )
102 103
 }

+ 61
- 145
components/page-panel/page-panel.tsx 查看文件

@@ -1,10 +1,16 @@
1
-import styled from 'styles'
2
-import * as ContextMenu from '@radix-ui/react-context-menu'
3 1
 import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
4
-
5
-import { breakpoints, IconWrapper, RowButton } from 'components/shared'
6
-import { CheckIcon, PlusIcon } from '@radix-ui/react-icons'
7
-import * as Panel from '../panel'
2
+import styled from 'styles'
3
+import {
4
+  breakpoints,
5
+  DropdownMenuButton,
6
+  DropdownMenuDivider,
7
+  RowButton,
8
+  MenuContent,
9
+  FloatingContainer,
10
+  IconButton,
11
+  IconWrapper,
12
+} from 'components/shared'
13
+import { MixerVerticalIcon, PlusIcon, CheckIcon } from '@radix-ui/react-icons'
8 14
 import state, { useSelector } from 'state'
9 15
 import { useEffect, useRef, useState } from 'react'
10 16
 
@@ -37,152 +43,62 @@ export default function PagePanel(): JSX.Element {
37 43
         }
38 44
       }}
39 45
     >
40
-      <PanelRoot dir="ltr">
41
-        <DropdownMenu.Trigger
42
-          as={RowButton}
43
-          bp={breakpoints}
44
-          variant="pageButton"
45
-        >
46
+      <FloatingContainer>
47
+        <RowButton as={DropdownMenu.Trigger} bp={breakpoints}>
46 48
           <span>{documentPages[currentPageId].name}</span>
47
-        </DropdownMenu.Trigger>
48
-        <DropdownMenu.Content sideOffset={8}>
49
-          <PanelRoot>
50
-            <DropdownMenu.RadioGroup
51
-              as={Content}
52
-              value={currentPageId}
53
-              onValueChange={(id) => {
54
-                setIsOpen(false)
55
-                state.send('CHANGED_PAGE', { id })
56
-              }}
57
-            >
58
-              {sorted.map(({ id, name }) => (
59
-                <ContextMenu.Root dir="ltr" key={id}>
60
-                  <ContextMenu.Trigger>
61
-                    <StyledRadioItem key={id} value={id} bp={breakpoints}>
62
-                      <span>{name}</span>
63
-                      <DropdownMenu.ItemIndicator as={IconWrapper} size="small">
64
-                        <CheckIcon />
65
-                      </DropdownMenu.ItemIndicator>
66
-                    </StyledRadioItem>
67
-                  </ContextMenu.Trigger>
68
-                  <StyledContextMenuContent>
69
-                    <ContextMenu.Group>
70
-                      <StyledContextMenuItem
71
-                        onSelect={() => state.send('RENAMED_PAGE', { id })}
72
-                      >
73
-                        Rename
74
-                      </StyledContextMenuItem>
75
-                      <StyledContextMenuItem
76
-                        onSelect={() => {
77
-                          setIsOpen(false)
78
-                          state.send('DELETED_PAGE', { id })
79
-                        }}
80
-                      >
81
-                        Delete
82
-                      </StyledContextMenuItem>
83
-                    </ContextMenu.Group>
84
-                  </StyledContextMenuContent>
85
-                </ContextMenu.Root>
86
-              ))}
87
-            </DropdownMenu.RadioGroup>
88
-            <DropdownMenu.Separator />
89
-            <RowButton
90
-              bp={breakpoints}
91
-              onClick={() => {
92
-                setIsOpen(false)
93
-                state.send('CREATED_PAGE')
94
-              }}
95
-            >
96
-              <span>Create Page</span>
97
-              <IconWrapper size="small">
98
-                <PlusIcon />
99
-              </IconWrapper>
100
-            </RowButton>
101
-          </PanelRoot>
102
-        </DropdownMenu.Content>
103
-      </PanelRoot>
49
+        </RowButton>
50
+      </FloatingContainer>
51
+      <MenuContent as={DropdownMenu.Content} sideOffset={8}>
52
+        <DropdownMenu.RadioGroup
53
+          value={currentPageId}
54
+          onChange={(id) => {
55
+            setIsOpen(false)
56
+            state.send('CHANGED_PAGE', { id })
57
+          }}
58
+        >
59
+          {sorted.map(({ id, name }) => (
60
+            <ButtonWithOptions key={id}>
61
+              <DropdownMenu.RadioItem
62
+                as={RowButton}
63
+                bp={breakpoints}
64
+                value={id}
65
+                variant="pageButton"
66
+              >
67
+                <span>{name}</span>
68
+                <DropdownMenu.ItemIndicator>
69
+                  <IconWrapper>
70
+                    <CheckIcon />
71
+                  </IconWrapper>
72
+                </DropdownMenu.ItemIndicator>
73
+              </DropdownMenu.RadioItem>
74
+              <IconButton bp={breakpoints} size="small" data-shy="true">
75
+                <MixerVerticalIcon />
76
+              </IconButton>
77
+            </ButtonWithOptions>
78
+          ))}
79
+        </DropdownMenu.RadioGroup>
80
+        <DropdownMenuDivider />
81
+        <DropdownMenuButton onSelect={() => state.send('CREATED_PAGE')}>
82
+          <span>Create Page</span>
83
+          <IconWrapper size="small">
84
+            <PlusIcon />
85
+          </IconWrapper>
86
+        </DropdownMenuButton>
87
+      </MenuContent>
104 88
     </DropdownMenu.Root>
105 89
   )
106 90
 }
107 91
 
108
-const PanelRoot = styled('div', {
109
-  zIndex: 200,
110
-  overflow: 'hidden',
111
-  position: 'relative',
112
-  display: 'flex',
113
-  flexDirection: 'column',
114
-  alignItems: 'center',
115
-  pointerEvents: 'all',
116
-  padding: '$0',
117
-  borderRadius: '4px',
118
-  backgroundColor: '$panel',
119
-  border: '1px solid $panel',
120
-  boxShadow: '$4',
121
-  userSelect: 'none',
122
-})
92
+const ButtonWithOptions = styled('div', {
93
+  display: 'grid',
94
+  gridTemplateColumns: '1fr auto',
95
+  gridAutoFlow: 'column',
123 96
 
124
-const Content = styled(Panel.Content, {
125
-  width: '100%',
126
-  minWidth: 128,
127
-})
128
-
129
-const StyledRadioItem = styled(DropdownMenu.RadioItem, {
130
-  height: 32,
131
-  width: 'auto',
132
-  display: 'flex',
133
-  alignItems: 'center',
134
-  justifyContent: 'space-between',
135
-  padding: '0 6px 0 12px',
136
-  cursor: 'pointer',
137
-  borderRadius: '4px',
138
-  fontSize: '$1',
139
-  fontFamily: '$ui',
140
-  fontWeight: 400,
141
-  backgroundColor: 'transparent',
142
-  outline: 'none',
143
-  variants: {
144
-    bp: {
145
-      mobile: {},
146
-      small: {
147
-        '&:hover': {
148
-          backgroundColor: '$hover',
149
-        },
150
-        '&:focus-within': {
151
-          backgroundColor: '$hover',
152
-        },
153
-      },
154
-    },
97
+  '& > *[data-shy="true"]': {
98
+    opacity: 0,
155 99
   },
156
-})
157
-
158
-const StyledContextMenuContent = styled(ContextMenu.Content, {
159
-  padding: '$0',
160
-  borderRadius: '4px',
161
-  backgroundColor: '$panel',
162
-  border: '1px solid $panel',
163
-  boxShadow: '$4',
164
-})
165 100
 
166
-const StyledContextMenuItem = styled(ContextMenu.Item, {
167
-  height: 32,
168
-  width: '100%',
169
-  display: 'flex',
170
-  alignItems: 'center',
171
-  justifyContent: 'space-between',
172
-  padding: '0 12px 0 12px',
173
-  cursor: 'pointer',
174
-  borderRadius: '4px',
175
-  fontSize: '$1',
176
-  fontFamily: '$ui',
177
-  fontWeight: 400,
178
-  backgroundColor: 'transparent',
179
-  outline: 'none',
180
-  bp: {
181
-    mobile: {},
182
-    small: {
183
-      '&:hover:not(:disabled)': {
184
-        backgroundColor: '$hover',
185
-      },
186
-    },
101
+  '&:hover > *[data-shy="true"]': {
102
+    opacity: 1,
187 103
   },
188 104
 })

+ 127
- 28
components/shared.tsx
文件差异内容过多而无法显示
查看文件


+ 3
- 2
components/status-bar.tsx 查看文件

@@ -35,12 +35,13 @@ const StatusBarContainer = styled('div', {
35 35
   zIndex: 300,
36 36
   height: 40,
37 37
   userSelect: 'none',
38
-  borderTop: '1px solid black',
38
+  borderTop: '1px solid $border',
39 39
   gridArea: 'status',
40 40
   display: 'grid',
41
+  color: '$text',
41 42
   gridTemplateColumns: 'auto 1fr auto',
42 43
   alignItems: 'center',
43
-  backgroundColor: 'white',
44
+  backgroundColor: '$panel',
44 45
   gap: 8,
45 46
   fontSize: '$0',
46 47
   padding: '0 16px',

+ 5
- 2
components/style-panel/color-content.tsx 查看文件

@@ -6,6 +6,7 @@ import { Square } from 'react-feather'
6 6
 import { DropdownContent } from '../shared'
7 7
 import { memo } from 'react'
8 8
 import state from 'state'
9
+import useTheme from 'hooks/useTheme'
9 10
 
10 11
 function handleColorChange(
11 12
   e: Event & { currentTarget: { value: ColorStyle } }
@@ -14,9 +15,11 @@ function handleColorChange(
14 15
 }
15 16
 
16 17
 function ColorContent(): JSX.Element {
18
+  const { theme } = useTheme()
19
+
17 20
   return (
18 21
     <DropdownContent sideOffset={8} side="bottom">
19
-      {Object.keys(strokes).map((color: ColorStyle) => (
22
+      {Object.keys(strokes[theme]).map((color: ColorStyle) => (
20 23
         <DropdownMenu.DropdownMenuItem
21 24
           as={IconButton}
22 25
           key={color}
@@ -24,7 +27,7 @@ function ColorContent(): JSX.Element {
24 27
           value={color}
25 28
           onSelect={handleColorChange}
26 29
         >
27
-          <Square fill={strokes[color]} stroke="none" size="22" />
30
+          <Square fill={strokes[theme][color]} stroke="none" size="22" />
28 31
         </DropdownMenu.DropdownMenuItem>
29 32
       ))}
30 33
     </DropdownContent>

+ 4
- 2
components/style-panel/quick-color-select.tsx 查看文件

@@ -1,19 +1,21 @@
1 1
 import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
2 2
 import { breakpoints, IconButton } from 'components/shared'
3 3
 import Tooltip from 'components/tooltip'
4
-import { strokes } from 'state/shape-styles'
4
+import { fills, strokes } from 'state/shape-styles'
5 5
 import { useSelector } from 'state'
6 6
 import ColorContent from './color-content'
7 7
 import { BoxIcon } from '../shared'
8
+import useTheme from 'hooks/useTheme'
8 9
 
9 10
 export default function QuickColorSelect(): JSX.Element {
10 11
   const color = useSelector((s) => s.values.selectedStyle.color)
12
+  const { theme } = useTheme()
11 13
 
12 14
   return (
13 15
     <DropdownMenu.Root dir="ltr">
14 16
       <DropdownMenu.Trigger as={IconButton} bp={breakpoints}>
15 17
         <Tooltip label="Color">
16
-          <BoxIcon fill={strokes[color]} stroke={strokes[color]} />
18
+          <BoxIcon fill={fills[theme][color]} stroke={strokes[theme][color]} />
17 19
         </Tooltip>
18 20
       </DropdownMenu.Trigger>
19 21
       <ColorContent />

+ 2
- 1
hooks/useTheme.ts 查看文件

@@ -1,9 +1,10 @@
1 1
 /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
2 2
 import { useCallback } from 'react'
3 3
 import state, { useSelector } from 'state'
4
+import { Theme } from 'types'
4 5
 
5 6
 export default function useTheme() {
6
-  const theme = useSelector((state) =>
7
+  const theme: Theme = useSelector((state) =>
7 8
     state.data.settings.isDarkMode ? 'dark' : 'light'
8 9
   )
9 10
 

+ 53
- 31
state/shape-styles.ts 查看文件

@@ -1,33 +1,50 @@
1
-import { ColorStyle, DashStyle, ShapeStyles, SizeStyle } from 'types'
2
-
3
-export const strokes: Record<ColorStyle, string> = {
4
-  [ColorStyle.White]: 'rgba(248, 249, 250, 1.000)',
5
-  [ColorStyle.LightGray]: 'rgba(224, 226, 230, 1.000)',
6
-  [ColorStyle.Gray]: 'rgba(172, 181, 189, 1.000)',
7
-  [ColorStyle.Black]: 'rgba(0,0,0, 1.000)',
8
-  [ColorStyle.Green]: 'rgba(54, 178, 77, 1.000)',
9
-  [ColorStyle.Cyan]: 'rgba(14, 152, 173, 1.000)',
10
-  [ColorStyle.Blue]: 'rgba(28, 126, 214, 1.000)',
11
-  [ColorStyle.Indigo]: 'rgba(66, 99, 235, 1.000)',
12
-  [ColorStyle.Violet]: 'rgba(112, 72, 232, 1.000)',
13
-  [ColorStyle.Red]: 'rgba(240, 63, 63, 1.000)',
14
-  [ColorStyle.Orange]: 'rgba(247, 103, 6, 1.000)',
15
-  [ColorStyle.Yellow]: 'rgba(245, 159, 0, 1.000)',
1
+import { Theme, ColorStyle, DashStyle, ShapeStyles, SizeStyle } from 'types'
2
+import { lerpColor } from 'utils'
3
+
4
+const canvasLight = '#fafafa'
5
+
6
+const canvasDark = '#343d45'
7
+
8
+const colors = {
9
+  [ColorStyle.White]: '#f0f1f3',
10
+  [ColorStyle.LightGray]: '#c6cbd1',
11
+  [ColorStyle.Gray]: '#788492',
12
+  [ColorStyle.Black]: '#212528',
13
+  [ColorStyle.Green]: '#36b24d',
14
+  [ColorStyle.Cyan]: '#0e98ad',
15
+  [ColorStyle.Blue]: '#1c7ed6',
16
+  [ColorStyle.Indigo]: '#4263eb',
17
+  [ColorStyle.Violet]: '#7746f1',
18
+  [ColorStyle.Red]: '#ff2133',
19
+  [ColorStyle.Orange]: '#ff9433',
20
+  [ColorStyle.Yellow]: '#ffc936',
16 21
 }
17 22
 
18
-export const fills = {
19
-  [ColorStyle.White]: 'rgba(224, 226, 230, 1.000)',
20
-  [ColorStyle.LightGray]: 'rgba(255, 255, 255, 1.000)',
21
-  [ColorStyle.Gray]: 'rgba(224, 226, 230, 1.000)',
22
-  [ColorStyle.Black]: 'rgba(255, 255, 255, 1.000)',
23
-  [ColorStyle.Green]: 'rgba(235, 251, 238, 1.000)',
24
-  [ColorStyle.Cyan]: 'rgba(227, 250, 251, 1.000)',
25
-  [ColorStyle.Blue]: 'rgba(231, 245, 255, 1.000)',
26
-  [ColorStyle.Indigo]: 'rgba(237, 242, 255, 1.000)',
27
-  [ColorStyle.Violet]: 'rgba(242, 240, 255, 1.000)',
28
-  [ColorStyle.Red]: 'rgba(255, 245, 245, 1.000)',
29
-  [ColorStyle.Orange]: 'rgba(255, 244, 229, 1.000)',
30
-  [ColorStyle.Yellow]: 'rgba(255, 249, 219, 1.000)',
23
+export const strokes: Record<Theme, Record<ColorStyle, string>> = {
24
+  light: colors,
25
+  dark: {
26
+    ...(Object.fromEntries(
27
+      Object.entries(colors).map(([k, v]) => [k, lerpColor(v, canvasDark, 0.1)])
28
+    ) as Record<ColorStyle, string>),
29
+    [ColorStyle.White]: '#ffffff',
30
+    [ColorStyle.Black]: '#000',
31
+  },
32
+}
33
+
34
+export const fills: Record<Theme, Record<ColorStyle, string>> = {
35
+  light: {
36
+    ...(Object.fromEntries(
37
+      Object.entries(colors).map(([k, v]) => [
38
+        k,
39
+        lerpColor(v, canvasLight, 0.82),
40
+      ])
41
+    ) as Record<ColorStyle, string>),
42
+    [ColorStyle.White]: '#ffffff',
43
+    [ColorStyle.Black]: '#ffffff',
44
+  },
45
+  dark: Object.fromEntries(
46
+    Object.entries(colors).map(([k, v]) => [k, lerpColor(v, canvasDark, 0.618)])
47
+  ) as Record<ColorStyle, string>,
31 48
 }
32 49
 
33 50
 const strokeWidths = {
@@ -57,7 +74,10 @@ export function getFontStyle(scale: number, style: ShapeStyles): string {
57 74
   return `${fontSize * scale}px/1.4 Verveine Regular`
58 75
 }
59 76
 
60
-export function getShapeStyle(style: ShapeStyles): {
77
+export function getShapeStyle(
78
+  style: ShapeStyles,
79
+  isDarkMode = false
80
+): {
61 81
   stroke: string
62 82
   fill: string
63 83
   strokeWidth: number
@@ -66,9 +86,11 @@ export function getShapeStyle(style: ShapeStyles): {
66 86
 
67 87
   const strokeWidth = getStrokeWidth(size)
68 88
 
89
+  const theme: Theme = isDarkMode ? 'dark' : 'light'
90
+
69 91
   return {
70
-    stroke: strokes[color],
71
-    fill: isFilled ? fills[color] : 'none',
92
+    stroke: strokes[theme][color],
93
+    fill: isFilled ? fills[theme][color] : 'none',
72 94
     strokeWidth,
73 95
   }
74 96
 }

+ 2
- 2
state/shape-utils/arrow.tsx 查看文件

@@ -101,7 +101,7 @@ const arrow = registerShapeUtils<ArrowShape>({
101 101
     return shape.handles !== prev.handles || shape.style !== prev.style
102 102
   },
103 103
 
104
-  render(shape) {
104
+  render(shape, { isDarkMode }) {
105 105
     const { bend, handles, style } = shape
106 106
     const { start, end, bend: _bend } = handles
107 107
 
@@ -110,7 +110,7 @@ const arrow = registerShapeUtils<ArrowShape>({
110 110
 
111 111
     const isDraw = shape.style.dash === DashStyle.Draw
112 112
 
113
-    const styles = getShapeStyle(style)
113
+    const styles = getShapeStyle(style, isDarkMode)
114 114
 
115 115
     const { strokeWidth } = styles
116 116
 

+ 2
- 2
state/shape-utils/dot.tsx 查看文件

@@ -19,8 +19,8 @@ const dot = registerShapeUtils<DotShape>({
19 19
     style: defaultStyle,
20 20
   },
21 21
 
22
-  render(shape) {
23
-    const styles = getShapeStyle(shape.style)
22
+  render(shape, { isDarkMode }) {
23
+    const styles = getShapeStyle(shape.style, isDarkMode)
24 24
 
25 25
     return <use href="#dot" stroke={styles.stroke} fill={styles.stroke} />
26 26
   },

+ 2
- 2
state/shape-utils/draw.tsx 查看文件

@@ -40,10 +40,10 @@ const draw = registerShapeUtils<DrawShape>({
40 40
     return shape.points !== prev.points || shape.style !== prev.style
41 41
   },
42 42
 
43
-  render(shape, { isHovered }) {
43
+  render(shape, { isHovered, isDarkMode }) {
44 44
     const { points, style } = shape
45 45
 
46
-    const styles = getShapeStyle(style)
46
+    const styles = getShapeStyle(style, isDarkMode)
47 47
 
48 48
     const strokeWidth = +styles.strokeWidth
49 49
 

+ 2
- 2
state/shape-utils/ellipse.tsx 查看文件

@@ -42,9 +42,9 @@ const ellipse = registerShapeUtils<EllipseShape>({
42 42
     )
43 43
   },
44 44
 
45
-  render(shape) {
45
+  render(shape, { isDarkMode }) {
46 46
     const { radiusX, radiusY, style } = shape
47
-    const styles = getShapeStyle(style)
47
+    const styles = getShapeStyle(style, isDarkMode)
48 48
     const strokeWidth = +styles.strokeWidth
49 49
 
50 50
     const rx = Math.max(0, radiusX - strokeWidth / 2)

+ 2
- 2
state/shape-utils/line.tsx 查看文件

@@ -26,12 +26,12 @@ const line = registerShapeUtils<LineShape>({
26 26
     return shape.direction !== prev.direction || shape.style !== prev.style
27 27
   },
28 28
 
29
-  render(shape, { isHovered }) {
29
+  render(shape, { isHovered, isDarkMode }) {
30 30
     const { id, direction } = shape
31 31
     const [x1, y1] = vec.add([0, 0], vec.mul(direction, 10000))
32 32
     const [x2, y2] = vec.sub([0, 0], vec.mul(direction, 10000))
33 33
 
34
-    const styles = getShapeStyle(shape.style)
34
+    const styles = getShapeStyle(shape.style, isDarkMode)
35 35
 
36 36
     return (
37 37
       <g id={id} filter={isHovered ? 'url(#expand)' : 'none'}>

+ 2
- 2
state/shape-utils/polyline.tsx 查看文件

@@ -28,10 +28,10 @@ const polyline = registerShapeUtils<PolylineShape>({
28 28
   shouldRender(shape, prev) {
29 29
     return shape.points !== prev.points || shape.style !== prev.style
30 30
   },
31
-  render(shape) {
31
+  render(shape, { isDarkMode }) {
32 32
     const { points, style } = shape
33 33
 
34
-    const styles = getShapeStyle(style)
34
+    const styles = getShapeStyle(style, isDarkMode)
35 35
 
36 36
     return (
37 37
       <polyline

+ 2
- 2
state/shape-utils/ray.tsx 查看文件

@@ -25,10 +25,10 @@ const ray = registerShapeUtils<RayShape>({
25 25
   shouldRender(shape, prev) {
26 26
     return shape.direction !== prev.direction || shape.style !== prev.style
27 27
   },
28
-  render(shape) {
28
+  render(shape, { isDarkMode }) {
29 29
     const { direction } = shape
30 30
 
31
-    const styles = getShapeStyle(shape.style)
31
+    const styles = getShapeStyle(shape.style, isDarkMode)
32 32
 
33 33
     const [x2, y2] = vec.add([0, 0], vec.mul(direction, 10000))
34 34
 

+ 2
- 2
state/shape-utils/rectangle.tsx 查看文件

@@ -28,9 +28,9 @@ const rectangle = registerShapeUtils<RectangleShape>({
28 28
     return shape.size !== prev.size || shape.style !== prev.style
29 29
   },
30 30
 
31
-  render(shape, { isHovered }) {
31
+  render(shape, { isHovered, isDarkMode }) {
32 32
     const { id, size, radius, style } = shape
33
-    const styles = getShapeStyle(style)
33
+    const styles = getShapeStyle(style, isDarkMode)
34 34
     const strokeWidth = +styles.strokeWidth
35 35
 
36 36
     if (style.dash === DashStyle.Draw) {

+ 2
- 2
state/shape-utils/text.tsx 查看文件

@@ -70,9 +70,9 @@ const text = registerShapeUtils<TextShape>({
70 70
     )
71 71
   },
72 72
 
73
-  render(shape, { isEditing, ref }) {
73
+  render(shape, { isEditing, isDarkMode, ref }) {
74 74
     const { id, text, style } = shape
75
-    const styles = getShapeStyle(style)
75
+    const styles = getShapeStyle(style, isDarkMode)
76 76
     const font = getFontStyle(shape.scale, shape.style)
77 77
 
78 78
     const bounds = this.getBounds(shape)

+ 22
- 8
state/state.ts 查看文件

@@ -8,6 +8,7 @@ import storage from './storage'
8 8
 import session from './session'
9 9
 import clipboard from './clipboard'
10 10
 import commands from './commands'
11
+import { dark, light } from 'styles'
11 12
 import {
12 13
   vec,
13 14
   getCommonBounds,
@@ -43,7 +44,7 @@ const initialData: Data = {
43 44
   settings: {
44 45
     fontSize: 13,
45 46
     isTestMode: false,
46
-    isDarkMode: false,
47
+    isDarkMode: true,
47 48
     isCodeOpen: false,
48 49
     isDebugMode: false,
49 50
     isDebugOpen: false,
@@ -144,6 +145,7 @@ for (let i = 0; i < count; i++) {
144 145
 
145 146
 const state = createState({
146 147
   data: initialData,
148
+  onEnter: 'applyTheme',
147 149
   on: {
148 150
     TOGGLED_DEBUG_PANEL: 'toggleDebugPanel',
149 151
     TOGGLED_DEBUG_MODE: 'toggleDebugMode',
@@ -168,12 +170,15 @@ const state = createState({
168 170
       },
169 171
     },
170 172
     ready: {
171
-      onEnter: {
172
-        wait: 0.01,
173
-        if: 'hasSelection',
174
-        do: 'zoomCameraToSelectionActual',
175
-        else: ['zoomCameraToActual'],
176
-      },
173
+      onEnter: [
174
+        'applyTheme',
175
+        {
176
+          wait: 0.01,
177
+          if: 'hasSelection',
178
+          do: 'zoomCameraToSelectionActual',
179
+          else: ['zoomCameraToActual'],
180
+        },
181
+      ],
177 182
       on: {
178 183
         UNMOUNTED: {
179 184
           do: ['saveDocumentState', 'resetDocumentState'],
@@ -204,7 +209,7 @@ const state = createState({
204 209
           do: 'pasteShapesFromClipboard',
205 210
         },
206 211
         TOGGLED_DARK_MODE: {
207
-          do: 'toggleDarkMode',
212
+          do: ['toggleDarkMode', 'applyTheme'],
208 213
         },
209 214
         TOGGLED_SHAPE_LOCK: {
210 215
           unlessAny: ['isReadOnly', 'isInSession'],
@@ -1971,6 +1976,15 @@ const state = createState({
1971 1976
     toggleDarkMode(data) {
1972 1977
       data.settings.isDarkMode = !data.settings.isDarkMode
1973 1978
     },
1979
+    applyTheme(data) {
1980
+      if (data.settings.isDarkMode && typeof document !== 'undefined') {
1981
+        document.body.classList.remove(light)
1982
+        document.body.classList.add(dark)
1983
+      } else {
1984
+        document.body.classList.remove(dark)
1985
+        document.body.classList.add(light)
1986
+      }
1987
+    },
1974 1988
 
1975 1989
     /* --------------------- Styles --------------------- */
1976 1990
 

+ 20
- 11
styles/stitches.config.ts 查看文件

@@ -96,26 +96,34 @@ const light = theme({})
96 96
 
97 97
 const dark = theme({
98 98
   colors: {
99
-    codeHl: 'rgba(144, 144, 144, .15)',
100 99
     brushFill: 'rgba(0,0,0,.05)',
101 100
     brushStroke: 'rgba(0,0,0,.25)',
102 101
     hint: 'rgba(216, 226, 249, 1.000)',
103
-    selected: 'rgba(66, 133, 244, 1.000)',
104
-    bounds: 'rgba(65, 132, 244, 1.000)',
105
-    boundsBg: 'rgba(65, 132, 244, 0.05)',
106
-    highlight: 'rgba(65, 132, 244, 0.15)',
102
+    selected: 'rgba(82, 143, 245, 1.000)',
103
+    bounds: 'rgba(82, 143, 245, 1.000)',
104
+    boundsBg: 'rgba(82, 143, 245, 0.05)',
105
+    highlight: 'rgba(82, 143, 245, 0.15)',
107 106
     overlay: 'rgba(0, 0, 0, 0.15)',
108
-    border: '#aaa',
109
-    canvas: '#fafafa',
110
-    panel: '#fefefe',
107
+    border: '#202529',
108
+    canvas: '#343d45',
109
+    panel: '#49555f',
111 110
     inactive: '#cccccf',
112
-    hover: '#efefef',
113
-    text: '#333',
114
-    muted: '#777',
111
+    hover: '#343d45',
112
+    text: '#f8f9fa',
113
+    muted: '#e0e2e6',
115 114
     input: '#f3f3f3',
116 115
     inputBorder: '#ddd',
116
+    codeHl: 'rgba(144, 144, 144, .15)',
117 117
     lineError: 'rgba(255, 0, 0, .1)',
118 118
   },
119
+  shadows: {
120
+    2: '0px 1px 1px rgba(0, 0, 0, 0.24)',
121
+    3: '0px 2px 3px rgba(0, 0, 0, 0.24)',
122
+    4: '0px 4px 5px -1px rgba(0, 0, 0, 0.24)',
123
+    8: '0px 12px 17px rgba(0, 0, 0, 0.24)',
124
+    12: '0px 12px 17px rgba(0, 0, 0, 0.24)',
125
+    24: '0px 24px 38px rgba(0, 0, 0, 0.24)',
126
+  },
119 127
 })
120 128
 
121 129
 const globalStyles = global({
@@ -130,6 +138,7 @@ const globalStyles = global({
130 138
     overscrollBehavior: 'none',
131 139
     fontFamily: '$ui',
132 140
     fontSize: '$2',
141
+    color: '$text',
133 142
     backgroundColor: '$canvas',
134 143
   },
135 144
   body: {

+ 3
- 0
types.ts 查看文件

@@ -124,6 +124,8 @@ export enum FontSize {
124 124
   ExtraLarge = 'ExtraLarge',
125 125
 }
126 126
 
127
+export type Theme = 'dark' | 'light'
128
+
127 129
 export type ShapeStyles = {
128 130
   color: ColorStyle
129 131
   size: SizeStyle
@@ -612,6 +614,7 @@ export interface ShapeUtility<K extends Shape> {
612 614
       isHovered?: boolean
613 615
       isSelected?: boolean
614 616
       isCurrentParent?: boolean
617
+      isDarkMode?: boolean
615 618
       ref?: React.MutableRefObject<HTMLTextAreaElement>
616 619
     }
617 620
   ): JSX.Element

+ 41
- 0
utils/utils.ts 查看文件

@@ -20,6 +20,47 @@ export function lerp(y1: number, y2: number, mu: number): number {
20 20
   return y1 * (1 - mu) + y2 * mu
21 21
 }
22 22
 
23
+/**
24
+ * Linear interpolation between two colors.
25
+ *
26
+ * ### Example
27
+ *
28
+ *```ts
29
+ * lerpColor("#000000", "#0099FF", .25)
30
+ *```
31
+ */
32
+function h2r(hex: string) {
33
+  const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
34
+  return result
35
+    ? [
36
+        parseInt(result[1], 16),
37
+        parseInt(result[2], 16),
38
+        parseInt(result[3], 16),
39
+      ]
40
+    : null
41
+}
42
+
43
+function r2h(rgb: number[]) {
44
+  return (
45
+    '#' +
46
+    ((1 << 24) + (rgb[0] << 16) + (rgb[1] << 8) + rgb[2]).toString(16).slice(1)
47
+  )
48
+}
49
+
50
+export function lerpColor(
51
+  color1: string,
52
+  color2: string,
53
+  factor = 0.5
54
+): string {
55
+  const c1 = h2r(color1)
56
+  const c2 = h2r(color2)
57
+  const result = c1.slice()
58
+  for (let i = 0; i < 3; i++) {
59
+    result[i] = Math.round(result[i] + factor * (c2[i] - c1[i]))
60
+  }
61
+  return r2h(result)
62
+}
63
+
23 64
 /**
24 65
  * Modulate a value between two ranges.
25 66
  * @param value

正在加载...
取消
保存