diff --git a/.env.production b/.env.production index d3d034a..af31a79 100644 --- a/.env.production +++ b/.env.production @@ -4,6 +4,6 @@ REACT_APP_STRIPE_API_KEY=pk_live_MJjzi5djj524Y7h3fL5PNh4e00a852XD51 REACT_APP_STRIPE_URL=https://payment.owlbear.rodeo REACT_APP_VERSION=$npm_package_version REACT_APP_PREVIEW=false -REACT_APP_LOGGING=false +REACT_APP_LOGGING=true REACT_APP_FATHOM_SITE_ID=VMSHBPKD -REACT_APP_SENTRY_DSN=https://5257021c3a114649baa5e3b8ba775bfe@o467475.ingest.sentry.io/5493956 +REACT_APP_SENTRY_DSN=https://d6d22c5233b54c4d91df8fa29d5ffeb0@o467475.ingest.sentry.io/5493956 diff --git a/package.json b/package.json index b753dc4..10abdb1 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "@dnd-kit/sortable": "^3.1.0", "@mitchemmc/dexie-export-import": "^1.0.1", "@msgpack/msgpack": "^2.4.1", - "@sentry/react": "^6.2.2", + "@sentry/integrations": "^6.3.0", + "@sentry/react": "^6.3.0", "@stripe/stripe-js": "^1.13.1", "@tensorflow/tfjs": "^3.3.0", "@testing-library/jest-dom": "^5.11.9", @@ -27,6 +28,7 @@ "file-saver": "^2.0.5", "fuse.js": "^6.4.6", "image-outline": "^0.1.0", + "intersection-observer": "^0.12.0", "konva": "^7.2.5", "lodash.clonedeep": "^4.5.0", "lodash.get": "^4.4.2", @@ -39,6 +41,7 @@ "raw.macro": "^0.4.2", "react": "^17.0.1", "react-dom": "^17.0.1", + "react-intersection-observer": "^8.32.0", "react-konva": "^17.0.1-3", "react-markdown": "4", "react-media": "^2.0.0-rc.1", @@ -49,6 +52,7 @@ "react-scripts": "^4.0.3", "react-select": "^4.2.1", "react-spring": "^8.0.27", + "react-textarea-autosize": "^8.3.3", "react-toast-notifications": "^2.4.3", "react-use-gesture": "^9.1.3", "shortid": "^2.2.15", diff --git a/src/components/Select.js b/src/components/Select.js index 5bc43fc..d39c797 100644 --- a/src/components/Select.js +++ b/src/components/Select.js @@ -67,6 +67,7 @@ function Select({ creatable, ...props }) { primary25: theme.colors.highlight, }, })} + captureMenuScroll={false} {...props} /> ); diff --git a/src/components/TextareaAutoSize.css b/src/components/TextareaAutoSize.css new file mode 100644 index 0000000..4868f44 --- /dev/null +++ b/src/components/TextareaAutoSize.css @@ -0,0 +1,22 @@ +.textarea-auto-size { + box-sizing: border-box; + margin: 0; + min-width: 0; + display: block; + width: 100%; + appearance: none; + font-size: inherit; + line-height: inherit; + border-radius: 4px; + color: inherit; + background-color: transparent; + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, + "Helvetica Neue", sans-serif; + padding: 4px; + border: none; + resize: none; +} + +.textarea-auto-size:focus { + outline: none; +} diff --git a/src/components/TextareaAutoSize.js b/src/components/TextareaAutoSize.js new file mode 100644 index 0000000..53e58aa --- /dev/null +++ b/src/components/TextareaAutoSize.js @@ -0,0 +1,8 @@ +import TextareaAutosize from "react-textarea-autosize"; +import "./TextareaAutoSize.css"; + +function StyledTextareaAutoSize(props) { + return ; +} + +export default StyledTextareaAutoSize; diff --git a/src/components/image/GlobalImageDrop.js b/src/components/image/GlobalImageDrop.js index b1909e6..10efcd6 100644 --- a/src/components/image/GlobalImageDrop.js +++ b/src/components/image/GlobalImageDrop.js @@ -1,5 +1,5 @@ import React, { useState, useRef } from "react"; -import { Flex, Text } from "theme-ui"; +import { Box, Flex, Text } from "theme-ui"; import { useToasts } from "react-toast-notifications"; import LoadingOverlay from "../LoadingOverlay"; @@ -171,39 +171,61 @@ function GlobalImageDrop({ children, onMapChange, onMapTokensStateCreate }) { {...overlayListeners} > + Drop as map + Drop as token diff --git a/src/components/map/Map.js b/src/components/map/Map.js index 33fa62d..af5e9bc 100644 --- a/src/components/map/Map.js +++ b/src/components/map/Map.js @@ -1,5 +1,6 @@ import React, { useState } from "react"; import { Box } from "theme-ui"; +import { useToasts } from "react-toast-notifications"; import MapControls from "./MapControls"; import MapInteraction from "./MapInteraction"; @@ -49,6 +50,8 @@ function Map({ disabledTokens, session, }) { + const { addToast } = useToasts(); + const { tokensById } = useTokenData(); const [selectedToolId, setSelectedToolId] = useState("move"); @@ -232,6 +235,7 @@ function Map({ onShapesCut={handleFogShapesCut} onShapesRemove={handleFogShapesRemove} onShapesEdit={handleFogShapesEdit} + onShapeError={addToast} active={selectedToolId === "fog"} toolSettings={settings.fog} editable={allowFogDrawing && !settings.fog.preview} diff --git a/src/components/map/MapFog.js b/src/components/map/MapFog.js index 3e94081..7614328 100644 --- a/src/components/map/MapFog.js +++ b/src/components/map/MapFog.js @@ -38,8 +38,10 @@ import { Tick, getRelativePointerPosition, } from "../../helpers/konva"; +import { keyBy } from "../../helpers/shared"; import SubtractShapeAction from "../../actions/SubtractShapeAction"; +import CutShapeAction from "../../actions/CutShapeAction"; import useSetting from "../../hooks/useSetting"; @@ -52,6 +54,7 @@ function MapFog({ onShapesCut, onShapesRemove, onShapesEdit, + onShapeError, active, toolSettings, editable, @@ -214,6 +217,8 @@ function MapFog({ ) { const cut = toolSettings.useFogCut; let drawingShapes = [drawingShape]; + + // Filter out hidden or visible shapes if single layer enabled if (!toolSettings.multilayer) { const shapesToSubtract = shapes.filter((shape) => cut ? !shape.visible : shape.visible @@ -228,22 +233,32 @@ function MapFog({ } if (drawingShapes.length > 0) { - drawingShapes = drawingShapes.map((shape) => { - if (cut) { - return { - id: shape.id, - type: shape.type, - data: shape.data, - }; - } else { - return { ...shape, color: "black" }; - } - }); - if (cut) { - onShapesCut(drawingShapes); + // Run a pre-emptive cut action to check whether we've cut anything + const cutAction = new CutShapeAction(drawingShapes); + const state = cutAction.execute(keyBy(shapes, "id")); + + if (Object.keys(state).length === shapes.length) { + onShapeError("No fog to cut"); + } else { + onShapesCut( + drawingShapes.map((shape) => ({ + id: shape.id, + type: shape.type, + data: shape.data, + })) + ); + } } else { - onShapesAdd(drawingShapes); + onShapesAdd( + drawingShapes.map((shape) => ({ ...shape, color: "black" })) + ); + } + } else { + if (cut) { + onShapeError("Fog already cut"); + } else { + onShapeError("Fog already placed"); } } setDrawingShape(null); @@ -373,6 +388,7 @@ function MapFog({ }; let polygonShapes = [polygonShape]; + // Filter out hidden or visible shapes if single layer enabled if (!toolSettings.multilayer) { const shapesToSubtract = shapes.filter((shape) => cut ? !shape.visible : shape.visible @@ -388,7 +404,15 @@ function MapFog({ if (polygonShapes.length > 0) { if (cut) { - onShapesCut(polygonShapes); + // Run a pre-emptive cut action to check whether we've cut anything + const cutAction = new CutShapeAction(polygonShapes); + const state = cutAction.execute(keyBy(shapes, "id")); + + if (Object.keys(state).length === shapes.length) { + onShapeError("No fog to cut"); + } else { + onShapesCut(polygonShapes); + } } else { onShapesAdd( polygonShapes.map((shape) => ({ @@ -399,10 +423,23 @@ function MapFog({ })) ); } + } else { + if (cut) { + onShapeError("Fog already cut"); + } else { + onShapeError("Fog already placed"); + } } setDrawingShape(null); - }, [toolSettings, drawingShape, onShapesCut, onShapesAdd, shapes]); + }, [ + toolSettings, + drawingShape, + onShapesCut, + onShapesAdd, + onShapeError, + shapes, + ]); // Add keyboard shortcuts function handleKeyDown(event) { diff --git a/src/components/map/MapMeasure.js b/src/components/map/MapMeasure.js index f9318c9..5e5acc6 100644 --- a/src/components/map/MapMeasure.js +++ b/src/components/map/MapMeasure.js @@ -44,7 +44,10 @@ function MapMeasure({ map, active }) { const gridScale = parseGridScale(active && grid.measurement.scale); - const snapPositionToGrid = useGridSnapping(); + const snapPositionToGrid = useGridSnapping( + grid.measurement.type === "euclidean" ? 0 : 1, + false + ); useEffect(() => { if (!active) { diff --git a/src/components/map/MapTiles.js b/src/components/map/MapTiles.js index 3adeba6..811bb0c 100644 --- a/src/components/map/MapTiles.js +++ b/src/components/map/MapTiles.js @@ -4,6 +4,7 @@ import MapTile from "./MapTile"; import MapTileGroup from "./MapTileGroup"; import SortableTiles from "../tile/SortableTiles"; +import SortableTilesDragOverlay from "../tile/SortableTilesDragOverlay"; import { getGroupItems } from "../../helpers/group"; @@ -20,21 +21,25 @@ function MapTiles({ mapsById, onMapEdit, onMapSelect, subgroup }) { function renderTile(group) { if (group.type === "item") { const map = mapsById[group.id]; - const isSelected = selectedGroupIds.includes(group.id); - const canEdit = - isSelected && selectMode === "single" && selectedGroupIds.length === 1; - return ( - canEdit && onMapSelect(group.id)} - canEdit={canEdit} - badges={[`${map.grid.size.x}x${map.grid.size.y}`]} - /> - ); + if (map) { + const isSelected = selectedGroupIds.includes(group.id); + const canEdit = + isSelected && + selectMode === "single" && + selectedGroupIds.length === 1; + return ( + canEdit && onMapSelect(group.id)} + canEdit={canEdit} + badges={[`${map.grid.size.x}x${map.grid.size.y}`]} + /> + ); + } } else { const isSelected = selectedGroupIds.includes(group.id); const items = getGroupItems(group); @@ -53,7 +58,12 @@ function MapTiles({ mapsById, onMapEdit, onMapSelect, subgroup }) { } } - return ; + return ( + <> + + + + ); } export default MapTiles; diff --git a/src/components/map/MapToken.js b/src/components/map/MapToken.js index 13aa769..8ceb93a 100644 --- a/src/components/map/MapToken.js +++ b/src/components/map/MapToken.js @@ -248,16 +248,20 @@ function MapToken({ hitFunc={() => {}} /> - - + {tokenState.statuses?.length > 0 ? ( + + ) : null} + {tokenState.label ? ( + + ) : null} ); diff --git a/src/components/note/Note.js b/src/components/note/Note.js index 461d8b1..343be01 100644 --- a/src/components/note/Note.js +++ b/src/components/note/Note.js @@ -15,7 +15,7 @@ import colors from "../../helpers/colors"; import usePrevious from "../../hooks/usePrevious"; import useGridSnapping from "../../hooks/useGridSnapping"; -const minTextSize = 16; +const defaultFontSize = 16; function Note({ note, @@ -118,7 +118,7 @@ function Note({ } } - const [fontSize, setFontSize] = useState(1); + const [fontScale, setFontScale] = useState(1); useEffect(() => { const text = textRef.current; @@ -127,10 +127,10 @@ function Note({ } function findFontSize() { - // Create an array from 1 / minTextSize of the note height to the full note height - const sizes = Array.from( + // Create an array from 1 / defaultFontSize of the note height to the full note height + let sizes = Array.from( { length: Math.ceil(noteHeight - notePadding * 2) }, - (_, i) => i + Math.ceil(noteHeight / minTextSize) + (_, i) => i + Math.ceil(noteHeight / defaultFontSize) ); if (sizes.length > 0) { @@ -144,8 +144,7 @@ function Note({ return prev; } }); - - setFontSize(size); + setFontScale(size / defaultFontSize); } } @@ -215,11 +214,14 @@ function Note({ } align="left" verticalAlign="middle" - padding={notePadding} - fontSize={fontSize} + padding={notePadding / fontScale} + fontSize={defaultFontSize} + // Scale font instead of changing font size to avoid kerning issues with Firefox + scaleX={fontScale} + scaleY={fontScale} + width={noteWidth / fontScale} + height={note.textOnly ? undefined : noteHeight / fontScale} wrap="word" - width={noteWidth} - height={note.textOnly ? undefined : noteHeight} /> {/* Use an invisible text block to work out text sizing */} diff --git a/src/components/note/NoteMenu.js b/src/components/note/NoteMenu.js index 9724c2c..17c272f 100644 --- a/src/components/note/NoteMenu.js +++ b/src/components/note/NoteMenu.js @@ -1,7 +1,8 @@ import React, { useEffect, useState } from "react"; -import { Box, Flex, Text, IconButton, Textarea } from "theme-ui"; +import { Box, Flex, Text, IconButton } from "theme-ui"; import Slider from "../Slider"; +import TextareaAutosize from "../TextareaAutoSize"; import MapMenu from "../map/MapMenu"; @@ -128,20 +129,12 @@ function NoteMenu({ }} sx={{ alignItems: "center" }} > -