Browse Source

Initial support for mobile devices (#787)

* Initial support for mobile devices

No editing yet, but UI looks nice and you can open the canvas menu

* Add support for editing shape color, etc

* Allow the mobile menus to cover the shape selector

* Hopefully fix test error

* Fix touch on canvas

* Fix safe area handling & remove unused Island
vanilla_orig
Jed Fox 5 years ago
parent
commit
7a7a73b78d
No account linked to committer's email address

+ 1
- 1
public/index.html View File

@@ -5,7 +5,7 @@
5 5
     <title>Excalidraw</title>
6 6
     <meta
7 7
       name="viewport"
8
-      content="width=device-width, initial-scale=1, shrink-to-fit=no"
8
+      content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover, shrink-to-fit=no"
9 9
     />
10 10
     <meta name="theme-color" content="#000000" />
11 11
     <!-- prettier-ignore -->

+ 2
- 0
src/actions/actionCanvas.tsx View File

@@ -7,6 +7,7 @@ import { ToolButton } from "../components/ToolButton";
7 7
 import { t } from "../i18n";
8 8
 import { getNormalizedZoom } from "../scene";
9 9
 import { KEYS } from "../keys";
10
+import useIsMobile from "../is-mobile";
10 11
 
11 12
 export const actionChangeViewBackgroundColor: Action = {
12 13
   name: "changeViewBackgroundColor",
@@ -43,6 +44,7 @@ export const actionClearCanvas: Action = {
43 44
       icon={trash}
44 45
       title={t("buttons.clearReset")}
45 46
       aria-label={t("buttons.clearReset")}
47
+      showAriaLabel={useIsMobile()}
46 48
       onClick={() => {
47 49
         if (window.confirm(t("alerts.clearReset"))) {
48 50
           // TODO: Defined globally, since file handles aren't yet serializable.

+ 3
- 0
src/actions/actionExport.tsx View File

@@ -5,6 +5,7 @@ import { saveAsJSON, loadFromJSON } from "../scene";
5 5
 import { load, save } from "../components/icons";
6 6
 import { ToolButton } from "../components/ToolButton";
7 7
 import { t } from "../i18n";
8
+import useIsMobile from "../is-mobile";
8 9
 
9 10
 export const actionChangeProjectName: Action = {
10 11
   name: "changeProjectName",
@@ -51,6 +52,7 @@ export const actionSaveScene: Action = {
51 52
       icon={save}
52 53
       title={t("buttons.save")}
53 54
       aria-label={t("buttons.save")}
55
+      showAriaLabel={useIsMobile()}
54 56
       onClick={() => updateData(null)}
55 57
     />
56 58
   ),
@@ -71,6 +73,7 @@ export const actionLoadScene: Action = {
71 73
       icon={load}
72 74
       title={t("buttons.load")}
73 75
       aria-label={t("buttons.load")}
76
+      showAriaLabel={useIsMobile()}
74 77
       onClick={() => {
75 78
         loadFromJSON()
76 79
           .then(({ elements, appState }) => {

+ 1
- 0
src/appState.ts View File

@@ -29,6 +29,7 @@ export function getDefaultAppState(): AppState {
29 29
     isResizing: false,
30 30
     selectionElement: null,
31 31
     zoom: 1,
32
+    openedMenu: null,
32 33
   };
33 34
 }
34 35
 

+ 2
- 0
src/components/ColorPicker.css View File

@@ -98,7 +98,9 @@
98 98
   box-sizing: content-box;
99 99
   border-radius: 0px 4px 4px 0px;
100 100
   float: left;
101
+  padding: 1px;
101 102
   padding-left: 0.5em;
103
+  appearance: none;
102 104
 }
103 105
 
104 106
 .color-picker-label-swatch {

+ 2
- 0
src/components/ExportDialog.tsx View File

@@ -17,6 +17,7 @@ import { KEYS } from "../keys";
17 17
 
18 18
 import { probablySupportsClipboardBlob } from "../clipboard";
19 19
 import { getSelectedElements, isSomeElementSelected } from "../scene";
20
+import useIsMobile from "../is-mobile";
20 21
 
21 22
 const scales = [1, 2, 3];
22 23
 const defaultScale = scales.includes(devicePixelRatio) ? devicePixelRatio : 1;
@@ -233,6 +234,7 @@ export function ExportDialog({
233 234
         icon={exportFile}
234 235
         type="button"
235 236
         aria-label={t("buttons.export")}
237
+        showAriaLabel={useIsMobile()}
236 238
         title={t("buttons.export")}
237 239
         ref={triggerButton}
238 240
       />

+ 4
- 0
src/components/ToolButton.tsx View File

@@ -14,6 +14,7 @@ type ToolButtonBaseProps = {
14 14
   id?: string;
15 15
   size?: ToolIconSize;
16 16
   keyBindingLabel?: string;
17
+  showAriaLabel?: boolean;
17 18
 };
18 19
 
19 20
 type ToolButtonProps =
@@ -48,6 +49,9 @@ export const ToolButton = React.forwardRef(function(
48 49
         <div className="ToolIcon__icon" aria-hidden="true">
49 50
           {props.icon || props.label}
50 51
         </div>
52
+        {props.showAriaLabel && (
53
+          <div className="ToolIcon__label">{props["aria-label"]}</div>
54
+        )}
51 55
       </button>
52 56
     );
53 57
   }

+ 15
- 11
src/components/ToolIcon.scss View File

@@ -1,13 +1,13 @@
1 1
 .ToolIcon {
2
-  display: inline-block;
2
+  display: inline-flex;
3
+  align-items: center;
3 4
   position: relative;
4 5
   font-family: Cascadia;
5 6
   cursor: pointer;
7
+  background-color: #e9ecef;
6 8
 }
7 9
 
8 10
 .ToolIcon__icon {
9
-  background-color: #e9ecef;
10
-
11 11
   width: 2.5rem;
12 12
   height: 2.5rem;
13 13
 
@@ -23,6 +23,10 @@
23 23
   }
24 24
 }
25 25
 
26
+.ToolIcon__label {
27
+  font-family: var(--ui-font);
28
+}
29
+
26 30
 .ToolIcon_size_s .ToolIcon__icon {
27 31
   width: 1.4rem;
28 32
   height: 1.4rem;
@@ -35,13 +39,13 @@
35 39
   margin: 0;
36 40
   font-size: inherit;
37 41
 
38
-  &:hover .ToolIcon__icon {
42
+  &:hover {
39 43
     background-color: #e9ecef;
40 44
   }
41
-  &:active .ToolIcon__icon {
45
+  &:active {
42 46
     background-color: #ced4da;
43 47
   }
44
-  &:focus .ToolIcon__icon {
48
+  &:focus {
45 49
     box-shadow: 0 0 0 2px #a5d8ff;
46 50
   }
47 51
 }
@@ -70,19 +74,19 @@
70 74
   align-items: center;
71 75
   justify-content: center;
72 76
   margin-left: 0.1rem;
77
+  background-color: transparent;
73 78
 
74 79
   .ToolIcon__icon {
75
-    background-color: transparent;
76 80
     width: 2rem;
77 81
     height: 2em;
78 82
   }
79
-  &:hover .ToolIcon__icon {
83
+  &:hover {
80 84
     background-color: transparent;
81 85
   }
82
-  &:active .ToolIcon__icon {
86
+  &:active {
83 87
     background-color: transparent;
84 88
   }
85
-  &:focus .ToolIcon__icon {
89
+  &:focus {
86 90
     box-shadow: none;
87 91
   }
88 92
 }
@@ -93,6 +97,6 @@
93 97
   right: 3px;
94 98
   font-size: 0.5em;
95 99
   color: #adb5bd; // OC GRAY 5
96
-  font-family: Arial, Helvetica, sans-serif;
100
+  font-family: var(--ui-font);
97 101
   user-select: none;
98 102
 }

+ 237
- 111
src/index.tsx View File

@@ -104,6 +104,7 @@ import { LanguageList } from "./components/LanguageList";
104 104
 import { Point } from "roughjs/bin/geometry";
105 105
 import { t, languages, setLanguage, getLanguage } from "./i18n";
106 106
 import { HintViewer } from "./components/HintViewer";
107
+import useIsMobile, { IsMobileProvider } from "./is-mobile";
107 108
 
108 109
 import { copyToAppClipboard, getClipboardContent } from "./clipboard";
109 110
 import { normalizeScroll } from "./scene/data";
@@ -135,6 +136,18 @@ const MOUSE_BUTTON = {
135 136
   SECONDARY: 2,
136 137
 };
137 138
 
139
+// Block pinch-zooming on iOS outside of the content area
140
+document.addEventListener(
141
+  "touchmove",
142
+  function(event) {
143
+    // @ts-ignore
144
+    if (event.scale !== 1) {
145
+      event.preventDefault();
146
+    }
147
+  },
148
+  { passive: false },
149
+);
150
+
138 151
 let lastMouseUp: ((e: any) => void) | null = null;
139 152
 
140 153
 export function viewportCoordsToSceneCoords(
@@ -211,64 +224,58 @@ const LayerUI = React.memo(
211 224
     language,
212 225
     setElements,
213 226
   }: LayerUIProps) => {
214
-    function renderCanvasActions() {
227
+    const isMobile = useIsMobile();
228
+
229
+    function renderExportDialog() {
215 230
       return (
216
-        <Stack.Col gap={4}>
217
-          <Stack.Row justifyContent={"space-between"}>
218
-            {actionManager.renderAction("loadScene")}
219
-            {actionManager.renderAction("saveScene")}
220
-            <ExportDialog
221
-              elements={elements}
222
-              appState={appState}
223
-              actionManager={actionManager}
224
-              onExportToPng={(exportedElements, scale) => {
225
-                if (canvas) {
226
-                  exportCanvas("png", exportedElements, canvas, {
227
-                    exportBackground: appState.exportBackground,
228
-                    name: appState.name,
229
-                    viewBackgroundColor: appState.viewBackgroundColor,
230
-                    scale,
231
-                  });
232
-                }
233
-              }}
234
-              onExportToSvg={(exportedElements, scale) => {
235
-                if (canvas) {
236
-                  exportCanvas("svg", exportedElements, canvas, {
237
-                    exportBackground: appState.exportBackground,
238
-                    name: appState.name,
239
-                    viewBackgroundColor: appState.viewBackgroundColor,
240
-                    scale,
241
-                  });
242
-                }
243
-              }}
244
-              onExportToClipboard={(exportedElements, scale) => {
245
-                if (canvas) {
246
-                  exportCanvas("clipboard", exportedElements, canvas, {
247
-                    exportBackground: appState.exportBackground,
248
-                    name: appState.name,
249
-                    viewBackgroundColor: appState.viewBackgroundColor,
250
-                    scale,
251
-                  });
252
-                }
253
-              }}
254
-              onExportToBackend={exportedElements => {
255
-                if (canvas) {
256
-                  exportCanvas(
257
-                    "backend",
258
-                    exportedElements.map(element => ({
259
-                      ...element,
260
-                      isSelected: false,
261
-                    })),
262
-                    canvas,
263
-                    appState,
264
-                  );
265
-                }
266
-              }}
267
-            />
268
-            {actionManager.renderAction("clearCanvas")}
269
-          </Stack.Row>
270
-          {actionManager.renderAction("changeViewBackgroundColor")}
271
-        </Stack.Col>
231
+        <ExportDialog
232
+          elements={elements}
233
+          appState={appState}
234
+          actionManager={actionManager}
235
+          onExportToPng={(exportedElements, scale) => {
236
+            if (canvas) {
237
+              exportCanvas("png", exportedElements, canvas, {
238
+                exportBackground: appState.exportBackground,
239
+                name: appState.name,
240
+                viewBackgroundColor: appState.viewBackgroundColor,
241
+                scale,
242
+              });
243
+            }
244
+          }}
245
+          onExportToSvg={(exportedElements, scale) => {
246
+            if (canvas) {
247
+              exportCanvas("svg", exportedElements, canvas, {
248
+                exportBackground: appState.exportBackground,
249
+                name: appState.name,
250
+                viewBackgroundColor: appState.viewBackgroundColor,
251
+                scale,
252
+              });
253
+            }
254
+          }}
255
+          onExportToClipboard={(exportedElements, scale) => {
256
+            if (canvas) {
257
+              exportCanvas("clipboard", exportedElements, canvas, {
258
+                exportBackground: appState.exportBackground,
259
+                name: appState.name,
260
+                viewBackgroundColor: appState.viewBackgroundColor,
261
+                scale,
262
+              });
263
+            }
264
+          }}
265
+          onExportToBackend={exportedElements => {
266
+            if (canvas) {
267
+              exportCanvas(
268
+                "backend",
269
+                exportedElements.map(element => ({
270
+                  ...element,
271
+                  isSelected: false,
272
+                })),
273
+                canvas,
274
+                appState,
275
+              );
276
+            }
277
+          }}
278
+        />
272 279
       );
273 280
     }
274 281
 
@@ -284,51 +291,49 @@ const LayerUI = React.memo(
284 291
       }
285 292
 
286 293
       return (
287
-        <Island padding={4}>
288
-          <div className="panelColumn">
289
-            {actionManager.renderAction("changeStrokeColor")}
290
-            {(hasBackground(elementType) ||
291
-              targetElements.some(element => hasBackground(element.type))) && (
292
-              <>
293
-                {actionManager.renderAction("changeBackgroundColor")}
294
-
295
-                {actionManager.renderAction("changeFillStyle")}
296
-              </>
297
-            )}
294
+        <div className="panelColumn">
295
+          {actionManager.renderAction("changeStrokeColor")}
296
+          {(hasBackground(elementType) ||
297
+            targetElements.some(element => hasBackground(element.type))) && (
298
+            <>
299
+              {actionManager.renderAction("changeBackgroundColor")}
300
+
301
+              {actionManager.renderAction("changeFillStyle")}
302
+            </>
303
+          )}
298 304
 
299
-            {(hasStroke(elementType) ||
300
-              targetElements.some(element => hasStroke(element.type))) && (
301
-              <>
302
-                {actionManager.renderAction("changeStrokeWidth")}
305
+          {(hasStroke(elementType) ||
306
+            targetElements.some(element => hasStroke(element.type))) && (
307
+            <>
308
+              {actionManager.renderAction("changeStrokeWidth")}
303 309
 
304
-                {actionManager.renderAction("changeSloppiness")}
305
-              </>
306
-            )}
310
+              {actionManager.renderAction("changeSloppiness")}
311
+            </>
312
+          )}
307 313
 
308
-            {(hasText(elementType) ||
309
-              targetElements.some(element => hasText(element.type))) && (
310
-              <>
311
-                {actionManager.renderAction("changeFontSize")}
314
+          {(hasText(elementType) ||
315
+            targetElements.some(element => hasText(element.type))) && (
316
+            <>
317
+              {actionManager.renderAction("changeFontSize")}
312 318
 
313
-                {actionManager.renderAction("changeFontFamily")}
314
-              </>
315
-            )}
319
+              {actionManager.renderAction("changeFontFamily")}
320
+            </>
321
+          )}
316 322
 
317
-            {actionManager.renderAction("changeOpacity")}
323
+          {actionManager.renderAction("changeOpacity")}
318 324
 
319
-            <fieldset>
320
-              <legend>{t("labels.layers")}</legend>
321
-              <div className="buttonList">
322
-                {actionManager.renderAction("sendToBack")}
323
-                {actionManager.renderAction("sendBackward")}
324
-                {actionManager.renderAction("bringToFront")}
325
-                {actionManager.renderAction("bringForward")}
326
-              </div>
327
-            </fieldset>
325
+          <fieldset>
326
+            <legend>{t("labels.layers")}</legend>
327
+            <div className="buttonList">
328
+              {actionManager.renderAction("sendToBack")}
329
+              {actionManager.renderAction("sendBackward")}
330
+              {actionManager.renderAction("bringToFront")}
331
+              {actionManager.renderAction("bringForward")}
332
+            </div>
333
+          </fieldset>
328 334
 
329
-            {actionManager.renderAction("deleteSelectedElements")}
330
-          </div>
331
-        </Island>
335
+          {actionManager.renderAction("deleteSelectedElements")}
336
+        </div>
332 337
       );
333 338
     }
334 339
 
@@ -378,7 +383,125 @@ const LayerUI = React.memo(
378 383
       );
379 384
     }
380 385
 
381
-    return (
386
+    const lockButton = (
387
+      <LockIcon
388
+        checked={appState.elementLocked}
389
+        onChange={() => {
390
+          setAppState({
391
+            elementLocked: !appState.elementLocked,
392
+            elementType: appState.elementLocked
393
+              ? "selection"
394
+              : appState.elementType,
395
+          });
396
+        }}
397
+        title={t("toolBar.lock")}
398
+      />
399
+    );
400
+
401
+    return isMobile ? (
402
+      <>
403
+        {appState.openedMenu === "canvas" ? (
404
+          <section
405
+            className="App-mobile-menu"
406
+            aria-labelledby="canvas-actions-title"
407
+          >
408
+            <h2 className="visually-hidden" id="canvas-actions-title">
409
+              {t("headings.canvasActions")}
410
+            </h2>
411
+            <div className="App-mobile-menu-scroller">
412
+              <Stack.Col gap={4}>
413
+                {actionManager.renderAction("loadScene")}
414
+                {actionManager.renderAction("saveScene")}
415
+                {renderExportDialog()}
416
+                {actionManager.renderAction("clearCanvas")}
417
+                {actionManager.renderAction("changeViewBackgroundColor")}
418
+              </Stack.Col>
419
+            </div>
420
+          </section>
421
+        ) : appState.openedMenu === "shape" ? (
422
+          <section
423
+            className="App-mobile-menu"
424
+            aria-labelledby="selected-shape-title"
425
+          >
426
+            <h2 className="visually-hidden" id="selected-shape-title">
427
+              {t("headings.selectedShapeActions")}
428
+            </h2>
429
+            <div className="App-mobile-menu-scroller">
430
+              {renderSelectedShapeActions(elements)}
431
+            </div>
432
+          </section>
433
+        ) : null}
434
+        <FixedSideContainer side="top">
435
+          <section aria-labelledby="shapes-title">
436
+            <Stack.Col gap={4} align="center">
437
+              <Stack.Row gap={1}>
438
+                <Island padding={1}>
439
+                  <h2 className="visually-hidden" id="shapes-title">
440
+                    {t("headings.shapes")}
441
+                  </h2>
442
+                  <Stack.Row gap={1}>{renderShapesSwitcher()}</Stack.Row>
443
+                </Island>
444
+              </Stack.Row>
445
+            </Stack.Col>
446
+          </section>
447
+        </FixedSideContainer>
448
+        <footer className="App-toolbar">
449
+          <div className="App-toolbar-content">
450
+            <ToolButton
451
+              type="button"
452
+              icon={
453
+                <span style={{ fontSize: "2em", marginTop: "-0.15em" }}>☰</span>
454
+              }
455
+              aria-label={t("buttons.menu")}
456
+              onClick={() =>
457
+                setAppState(({ openedMenu }: any) => ({
458
+                  openedMenu: openedMenu === "canvas" ? null : "canvas",
459
+                }))
460
+              }
461
+            />
462
+            {lockButton}
463
+            <div
464
+              style={{
465
+                visibility: isSomeElementSelected(elements)
466
+                  ? "visible"
467
+                  : "hidden",
468
+              }}
469
+            >
470
+              <ToolButton
471
+                type="button"
472
+                icon={
473
+                  <span style={{ fontSize: "2em", marginTop: "-0.15em" }}>
474
+                    ✎
475
+                  </span>
476
+                }
477
+                aria-label={t("buttons.menu")}
478
+                onClick={() =>
479
+                  setAppState(({ openedMenu }: any) => ({
480
+                    openedMenu: openedMenu === "shape" ? null : "shape",
481
+                  }))
482
+                }
483
+              />
484
+            </div>
485
+            <HintViewer
486
+              elementType={appState.elementType}
487
+              multiMode={appState.multiElement !== null}
488
+              isResizing={appState.isResizing}
489
+              elements={elements}
490
+            />
491
+            {appState.scrolledOutside && (
492
+              <button
493
+                className="scroll-back-to-content"
494
+                onClick={() => {
495
+                  setAppState({ ...calculateScrollCenter(elements) });
496
+                }}
497
+              >
498
+                {t("buttons.scrollBackToContent")}
499
+              </button>
500
+            )}
501
+          </div>
502
+        </footer>
503
+      </>
504
+    ) : (
382 505
       <>
383 506
         <FixedSideContainer side="top">
384 507
           <div className="App-menu App-menu_top">
@@ -390,7 +513,17 @@ const LayerUI = React.memo(
390 513
                 <h2 className="visually-hidden" id="canvas-actions-title">
391 514
                   {t("headings.canvasActions")}
392 515
                 </h2>
393
-                <Island padding={4}>{renderCanvasActions()}</Island>
516
+                <Island padding={4}>
517
+                  <Stack.Col gap={4}>
518
+                    <Stack.Row justifyContent={"space-between"}>
519
+                      {actionManager.renderAction("loadScene")}
520
+                      {actionManager.renderAction("saveScene")}
521
+                      {renderExportDialog()}
522
+                      {actionManager.renderAction("clearCanvas")}
523
+                    </Stack.Row>
524
+                    {actionManager.renderAction("changeViewBackgroundColor")}
525
+                  </Stack.Col>
526
+                </Island>
394 527
               </section>
395 528
               <section
396 529
                 className="App-right-menu"
@@ -399,7 +532,9 @@ const LayerUI = React.memo(
399 532
                 <h2 className="visually-hidden" id="selected-shape-title">
400 533
                   {t("headings.selectedShapeActions")}
401 534
                 </h2>
402
-                {renderSelectedShapeActions(elements)}
535
+                <Island padding={4}>
536
+                  {renderSelectedShapeActions(elements)}
537
+                </Island>
403 538
               </section>
404 539
             </Stack.Col>
405 540
             <section aria-labelledby="shapes-title">
@@ -411,18 +546,7 @@ const LayerUI = React.memo(
411 546
                     </h2>
412 547
                     <Stack.Row gap={1}>{renderShapesSwitcher()}</Stack.Row>
413 548
                   </Island>
414
-                  <LockIcon
415
-                    checked={appState.elementLocked}
416
-                    onChange={() => {
417
-                      setAppState({
418
-                        elementLocked: !appState.elementLocked,
419
-                        elementType: appState.elementLocked
420
-                          ? "selection"
421
-                          : appState.elementType,
422
-                      });
423
-                    }}
424
-                    title={t("toolBar.lock")}
425
-                  />
549
+                  {lockButton}
426 550
                 </Stack.Row>
427 551
               </Stack.Col>
428 552
             </section>
@@ -2204,7 +2328,9 @@ class TopErrorBoundary extends React.Component {
2204 2328
 
2205 2329
 ReactDOM.render(
2206 2330
   <TopErrorBoundary>
2207
-    <App />
2331
+    <IsMobileProvider>
2332
+      <App />
2333
+    </IsMobileProvider>
2208 2334
   </TopErrorBoundary>,
2209 2335
   rootElement,
2210 2336
 );

+ 25
- 0
src/is-mobile.tsx View File

@@ -0,0 +1,25 @@
1
+import React, { useState, useEffect, useRef, useContext } from "react";
2
+
3
+const context = React.createContext(false);
4
+
5
+export function IsMobileProvider({ children }: { children: React.ReactNode }) {
6
+  const query = useRef<MediaQueryList>();
7
+  if (!query.current) {
8
+    query.current = window.matchMedia(
9
+      "(max-width: 600px), (max-height: 500px)",
10
+    );
11
+  }
12
+  const [isMobile, setMobile] = useState(query.current.matches);
13
+
14
+  useEffect(() => {
15
+    const handler = () => setMobile(query.current!.matches);
16
+    query.current!.addListener(handler);
17
+    return () => query.current!.removeListener(handler);
18
+  }, []);
19
+
20
+  return <context.Provider value={isMobile}>{children}</context.Provider>;
21
+}
22
+
23
+export default function useIsMobile() {
24
+  return useContext(context);
25
+}

+ 3
- 2
src/locales/en.json View File

@@ -43,7 +43,7 @@
43 43
     "layers": "Layers"
44 44
   },
45 45
   "buttons": {
46
-    "clearReset": "Clear the canvas & reset background color",
46
+    "clearReset": "Reset the canvas",
47 47
     "export": "Export",
48 48
     "exportToPng": "Export to PNG",
49 49
     "exportToSvg": "Export to SVG",
@@ -55,7 +55,8 @@
55 55
     "selectLanguage": "Select Language",
56 56
     "scrollBackToContent": "Scroll back to content",
57 57
     "zoomIn": "Zoom in",
58
-    "zoomOut": "Zoom out"
58
+    "zoomOut": "Zoom out",
59
+    "menu": "Menu"
59 60
   },
60 61
   "alerts": {
61 62
     "clearReset": "This will clear the whole canvas. Are you sure?",

+ 63
- 1
src/styles.scss View File

@@ -2,11 +2,16 @@
2 2
 
3 3
 body {
4 4
   margin: 0;
5
-  font-family: Arial, Helvetica, sans-serif;
5
+  --ui-font: Arial, Helvetica, sans-serif;
6
+  font-family: var(--ui-font);
6 7
   color: var(--text-color-primary);
8
+  -webkit-text-size-adjust: 100%;
7 9
 }
8 10
 
9 11
 canvas {
12
+  touch-action: none;
13
+  user-select: none;
14
+
10 15
   // following props improve blurriness at certain devicePixelRatios.
11 16
   // AFAIK it doesn't affect export (in fact, export seems sharp either way).
12 17
 
@@ -24,6 +29,11 @@ canvas {
24 29
   right: 0;
25 30
 }
26 31
 
32
+.panelRow {
33
+  display: flex;
34
+  justify-content: space-between;
35
+}
36
+
27 37
 .panelColumn {
28 38
   display: flex;
29 39
   flex-direction: column;
@@ -91,6 +101,7 @@ input:focus {
91 101
 
92 102
 button,
93 103
 .buttonList label {
104
+  user-select: none;
94 105
   background-color: #e9ecef;
95 106
   border: 0;
96 107
   border-radius: 4px;
@@ -128,6 +139,47 @@ button,
128 139
   }
129 140
 }
130 141
 
142
+.App-toolbar {
143
+  padding: var(--spacing);
144
+  padding-bottom: #{"max(var(--spacing), env(safe-area-inset-bottom))"};
145
+  padding-left: #{"max(var(--spacing), env(safe-area-inset-left))"};
146
+  padding-right: #{"max(var(--spacing), env(safe-area-inset-right))"};
147
+  width: 100%;
148
+  box-sizing: border-box;
149
+  overflow: auto;
150
+  position: absolute;
151
+  bottom: 0;
152
+}
153
+.App-toolbar-content {
154
+  display: flex;
155
+  align-items: center;
156
+  justify-content: space-between;
157
+}
158
+.App-toolbar,
159
+.App-mobile-menu {
160
+  --spacing: 0.5rem;
161
+  background: #fcfcfc;
162
+  border-top: 1px solid #ccc;
163
+}
164
+.App-mobile-menu {
165
+  --bottom: calc(3rem - 1px + max(var(--spacing), env(safe-area-inset-bottom)));
166
+  display: grid;
167
+  position: fixed;
168
+  width: 100%;
169
+  bottom: var(--bottom);
170
+  z-index: 4;
171
+  max-height: calc(100% - var(--bottom));
172
+  overflow-y: scroll;
173
+}
174
+.App-mobile-menu .App-mobile-menu-scroller {
175
+  background: #fcfcfc;
176
+  box-shadow: none;
177
+  --padding: calc(4 * var(--space-factor));
178
+  padding: var(--padding);
179
+  padding-left: #{"max(var(--padding), env(safe-area-inset-left))"};
180
+  padding-right: #{"max(var(--padding), env(safe-area-inset-right))"};
181
+}
182
+
131 183
 .App-menu {
132 184
   display: grid;
133 185
 }
@@ -303,3 +355,13 @@ button,
303 355
   transform: translateX(-50%);
304 356
   padding: 10px 20px;
305 357
 }
358
+
359
+@media (max-width: 600px), (max-height: 500px) {
360
+  aside {
361
+    display: none;
362
+  }
363
+  .scroll-back-to-content {
364
+    bottom: 70px;
365
+    bottom: calc(70px + env(safe-area-inset-bottom));
366
+  }
367
+}

+ 1
- 0
src/types.ts View File

@@ -31,4 +31,5 @@ export type AppState = {
31 31
   selectedId?: string;
32 32
   isResizing: boolean;
33 33
   zoom: number;
34
+  openedMenu: "canvas" | "shape" | null;
34 35
 };

Loading…
Cancel
Save