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" }}
>
-
+ {inView ? (
+ children
+ ) : (
+
+ )}
+
+ );
+}
+
+export default LazyTile;
diff --git a/src/components/tile/SortableTiles.js b/src/components/tile/SortableTiles.js
index 257cc6e..a42bfbd 100644
--- a/src/components/tile/SortableTiles.js
+++ b/src/components/tile/SortableTiles.js
@@ -1,26 +1,25 @@
import React from "react";
-import { createPortal } from "react-dom";
-import { DragOverlay } from "@dnd-kit/core";
import { SortableContext } from "@dnd-kit/sortable";
-import { animated, useSpring, config } from "react-spring";
-import { Badge } from "theme-ui";
import { moveGroupsInto } from "../../helpers/group";
import { keyBy } from "../../helpers/shared";
-import Vector2 from "../../helpers/Vector2";
import SortableTile from "./SortableTile";
+import LazyTile from "./LazyTile";
import {
- useTileDrag,
+ useTileDragId,
+ useTileDragCursor,
+ useTileOverGroupId,
BASE_SORTABLE_ID,
GROUP_SORTABLE_ID,
- GROUP_ID_PREFIX,
} from "../../contexts/TileDragContext";
import { useGroup } from "../../contexts/GroupContext";
function SortableTiles({ renderTile, subgroup }) {
- const { dragId, overId, dragCursor } = useTileDrag();
+ const dragId = useTileDragId();
+ const dragCursor = useTileDragCursor();
+ const overGroupId = useTileOverGroupId();
const {
groups,
selectedGroupIds: allSelectedIds,
@@ -46,15 +45,6 @@ function SortableTiles({ renderTile, subgroup }) {
const disableSorting = (openGroupId && !subgroup) || filter;
const disableGrouping = subgroup || disableSorting || filter;
- const dragBounce = useSpring({
- transform: !!dragId ? "scale(0.9)" : "scale(1)",
- config: config.wobbly,
- position: "relative",
- });
-
- const overGroupId =
- overId && overId.startsWith(GROUP_ID_PREFIX) && overId.slice(9);
-
function renderSortableGroup(group, selectedGroups) {
if (overGroupId === group.id && dragId && group.id !== dragId) {
// If dragging over a group render a preview of that group
@@ -68,57 +58,6 @@ function SortableTiles({ renderTile, subgroup }) {
return renderTile(group);
}
- function renderDragOverlays() {
- let selectedIndices = selectedGroupIds.map((groupId) =>
- activeGroups.findIndex((group) => group.id === groupId)
- );
- const activeIndex = activeGroups.findIndex((group) => group.id === dragId);
- // Sort so the draging tile is the first element
- selectedIndices = selectedIndices.sort((a, b) =>
- a === activeIndex ? -1 : b === activeIndex ? 1 : 0
- );
-
- selectedIndices = selectedIndices.slice(0, 5);
-
- let coords = selectedIndices.map(
- (_, index) => new Vector2(5 * index, 5 * index)
- );
-
- // Reverse so the first element is rendered on top
- selectedIndices = selectedIndices.reverse();
- coords = coords.reverse();
-
- const selectedGroups = selectedIndices.map((index) => activeGroups[index]);
-
- return selectedGroups.map((group, index) => (
-
-
-
- {renderTile(group)}
- {index === selectedIndices.length - 1 &&
- selectedGroupIds.length > 1 && (
-
- {selectedGroupIds.length}
-
- )}
-
-
-
- ));
- }
-
function renderTiles() {
const groupsByIds = keyBy(activeGroups, "id");
const selectedGroupIdsSet = new Set(selectedGroupIds);
@@ -138,17 +77,18 @@ function SortableTiles({ renderTile, subgroup }) {
const disableTileGrouping =
disableGrouping || isDragging || hasSelectedContainerGroup;
return (
-
- {renderSortableGroup(group, selectedGroups)}
-
+
+
+ {renderSortableGroup(group, selectedGroups)}
+
+
);
});
}
@@ -156,7 +96,6 @@ function SortableTiles({ renderTile, subgroup }) {
return (
{renderTiles()}
- {createPortal(dragId && renderDragOverlays(), document.body)}
);
}
diff --git a/src/components/tile/SortableTilesDragOverlay.js b/src/components/tile/SortableTilesDragOverlay.js
new file mode 100644
index 0000000..848b793
--- /dev/null
+++ b/src/components/tile/SortableTilesDragOverlay.js
@@ -0,0 +1,93 @@
+import React from "react";
+import { createPortal } from "react-dom";
+import { DragOverlay } from "@dnd-kit/core";
+import { animated, useSpring, config } from "react-spring";
+import { Badge } from "theme-ui";
+
+import Vector2 from "../../helpers/Vector2";
+
+import { useTileDragId } from "../../contexts/TileDragContext";
+import { useGroup } from "../../contexts/GroupContext";
+
+function SortableTilesDragOverlay({ renderTile, subgroup }) {
+ const dragId = useTileDragId();
+ const {
+ groups,
+ selectedGroupIds: allSelectedIds,
+ filter,
+ openGroupId,
+ openGroupItems,
+ filteredGroupItems,
+ } = useGroup();
+
+ const activeGroups = subgroup
+ ? openGroupItems
+ : filter
+ ? filteredGroupItems
+ : groups;
+
+ // Only populate selected groups if needed
+ let selectedGroupIds = [];
+ if ((subgroup && openGroupId) || (!subgroup && !openGroupId)) {
+ selectedGroupIds = allSelectedIds;
+ }
+ const dragBounce = useSpring({
+ transform: !!dragId ? "scale(0.9)" : "scale(1)",
+ config: config.wobbly,
+ position: "relative",
+ });
+
+ function renderDragOverlays() {
+ let selectedIndices = selectedGroupIds.map((groupId) =>
+ activeGroups.findIndex((group) => group.id === groupId)
+ );
+ const activeIndex = activeGroups.findIndex((group) => group.id === dragId);
+ // Sort so the draging tile is the first element
+ selectedIndices = selectedIndices.sort((a, b) =>
+ a === activeIndex ? -1 : b === activeIndex ? 1 : 0
+ );
+
+ selectedIndices = selectedIndices.slice(0, 5);
+
+ let coords = selectedIndices.map(
+ (_, index) => new Vector2(5 * index, 5 * index)
+ );
+
+ // Reverse so the first element is rendered on top
+ selectedIndices = selectedIndices.reverse();
+ coords = coords.reverse();
+
+ const selectedGroups = selectedIndices.map((index) => activeGroups[index]);
+
+ return selectedGroups.map((group, index) => (
+
+
+
+ {renderTile(group)}
+ {index === selectedIndices.length - 1 &&
+ selectedGroupIds.length > 1 && (
+
+ {selectedGroupIds.length}
+
+ )}
+
+
+
+ ));
+ }
+
+ return createPortal(dragId && renderDragOverlays(), document.body);
+}
+
+export default SortableTilesDragOverlay;
diff --git a/src/components/tile/Tile.js b/src/components/tile/Tile.js
index 610c19e..c550657 100644
--- a/src/components/tile/Tile.js
+++ b/src/components/tile/Tile.js
@@ -15,15 +15,13 @@ function Tile({
children,
}) {
return (
-
)}
-
+
);
}
diff --git a/src/components/token/TokenBar.js b/src/components/token/TokenBar.js
index ea4ef85..d59e221 100644
--- a/src/components/token/TokenBar.js
+++ b/src/components/token/TokenBar.js
@@ -1,6 +1,6 @@
import React, { useState } from "react";
import { createPortal } from "react-dom";
-import { Box, Flex } from "theme-ui";
+import { Box, Flex, Grid } from "theme-ui";
import SimpleBar from "simplebar-react";
import {
DragOverlay,
@@ -165,9 +165,16 @@ function TokenBar({ onMapTokensStateCreate }) {
padding: "0 16px",
}}
>
-
+
{tokenGroups.map((group) => renderToken(group))}
-
+
-
+
+ {inView && (
+
+ )}
);
}
diff --git a/src/components/token/TokenBarTokenGroup.js b/src/components/token/TokenBarTokenGroup.js
index d673ec1..7b36d60 100644
--- a/src/components/token/TokenBarTokenGroup.js
+++ b/src/components/token/TokenBarTokenGroup.js
@@ -40,11 +40,10 @@ function TokenBarTokenGroup({ group, tokens, draggable }) {
return (
handleOpenClick(e, false)}
key="group"
- alt={group.name}
title={group.name}
{...listeners}
{...attributes}
@@ -71,8 +69,6 @@ function TokenBarTokenGroup({ group, tokens, draggable }) {
return (
diff --git a/src/components/token/TokenEditBar.js b/src/components/token/TokenEditBar.js
index a759739..1150269 100644
--- a/src/components/token/TokenEditBar.js
+++ b/src/components/token/TokenEditBar.js
@@ -48,7 +48,14 @@ function TokenEditBar({ disabled, onLoad }) {
async function handleTokensHide(hideInSidebar) {
const selectedTokens = getSelectedTokens();
const selectedTokenIds = selectedTokens.map((token) => token.id);
- updateTokensHidden(selectedTokenIds, hideInSidebar);
+ // Show loading indicator if hiding more than 10 tokens
+ if (selectedTokenIds.length > 10) {
+ onLoad(true);
+ await updateTokensHidden(selectedTokenIds, hideInSidebar);
+ onLoad(false);
+ } else {
+ updateTokensHidden(selectedTokenIds, hideInSidebar);
+ }
}
/**
diff --git a/src/components/token/TokenLabel.js b/src/components/token/TokenLabel.js
index e11918e..b178149 100644
--- a/src/components/token/TokenLabel.js
+++ b/src/components/token/TokenLabel.js
@@ -4,6 +4,7 @@ import { Rect, Text, Group } from "react-konva";
import useSetting from "../../hooks/useSetting";
const maxTokenSize = 3;
+const defaultFontSize = 16;
function TokenLabel({ tokenState, width, height }) {
const [labelSize] = useSetting("map.labelSize");
@@ -13,7 +14,7 @@ function TokenLabel({ tokenState, width, height }) {
const paddingX =
(height / 8 / tokenState.size) * Math.min(tokenState.size, maxTokenSize);
- const [fontSize, setFontSize] = useState(1);
+ const [fontScale, setFontScale] = useState(0);
useEffect(() => {
const text = textSizerRef.current;
@@ -22,15 +23,14 @@ function TokenLabel({ tokenState, width, height }) {
}
let fontSizes = [];
- for (let size = 10 * labelSize; size >= 6; size--) {
- fontSizes.push(
- (height / size / tokenState.size) *
- Math.min(tokenState.size, maxTokenSize) *
- labelSize
- );
+ for (let size = 20 * labelSize; size >= 6; size--) {
+ const verticalSize = height / size / tokenState.size;
+ const tokenSize = Math.min(tokenState.size, maxTokenSize);
+ const fontSize = verticalSize * tokenSize * labelSize;
+ fontSizes.push(fontSize);
}
- function findFontSize() {
+ function findFontScale() {
const size = fontSizes.reduce((prev, curr) => {
text.fontSize(curr);
const textWidth = text.getTextWidth() + paddingX * 2;
@@ -39,12 +39,12 @@ function TokenLabel({ tokenState, width, height }) {
} else {
return prev;
}
- });
+ }, 1);
- setFontSize(size);
+ setFontScale(size / defaultFontSize);
}
- findFontSize();
+ findFontScale();
}, [
tokenState.label,
tokenState.visible,
@@ -56,44 +56,47 @@ function TokenLabel({ tokenState, width, height }) {
]);
const [rectWidth, setRectWidth] = useState(0);
+ const [textWidth, setTextWidth] = useState(0);
useEffect(() => {
const text = textRef.current;
if (text && tokenState.label) {
- setRectWidth(text.getTextWidth() + paddingX * 2);
+ setRectWidth(text.getTextWidth() * fontScale + paddingX * 2);
+ setTextWidth(text.getTextWidth() * fontScale);
} else {
setRectWidth(0);
+ setTextWidth(0);
}
- }, [tokenState.label, paddingX, width, fontSize]);
+ }, [tokenState.label, paddingX, width, fontScale]);
const textRef = useRef();
const textSizerRef = useRef();
return (
-
+
- {}}
+ cornerRadius={(defaultFontSize * fontScale + paddingY) / 2}
/>
+
+ {}}
+ />
+
{/* Use an invisible text block to work out text sizing */}
,
- ]}
- />
- );
+ return (
+ ,
+ ]}
+ />
+ );
+ }
} else {
const isSelected = selectedGroupIds.includes(group.id);
const items = getGroupItems(group);
@@ -57,7 +62,12 @@ function TokenTiles({ tokensById, onTokenEdit, subgroup }) {
}
}
- return ;
+ return (
+ <>
+
+
+ >
+ );
}
export default TokenTiles;
diff --git a/src/contexts/TileDragContext.js b/src/contexts/TileDragContext.js
index 6f6644e..3663f5a 100644
--- a/src/contexts/TileDragContext.js
+++ b/src/contexts/TileDragContext.js
@@ -1,4 +1,4 @@
-import React, { useState, useContext } from "react";
+import React, { useState, useContext, useEffect } from "react";
import {
MouseSensor,
TouchSensor,
@@ -16,7 +16,9 @@ import { moveGroupsInto, moveGroups, ungroup } from "../helpers/group";
import usePreventSelect from "../hooks/usePreventSelect";
-const TileDragContext = React.createContext();
+const TileDragIdContext = React.createContext();
+const TileOverGroupIdContext = React.createContext();
+const TileDragCursorContext = React.createContext();
export const BASE_SORTABLE_ID = "__base__";
export const GROUP_SORTABLE_ID = "__group__";
@@ -61,7 +63,7 @@ export function TileDragProvider({
} = useGroup();
const mouseSensor = useSensor(MouseSensor, {
- activationConstraint: { delay: 250, tolerance: 5 },
+ activationConstraint: { distance: 3 },
});
const touchSensor = useSensor(TouchSensor, {
activationConstraint: { delay: 250, tolerance: 5 },
@@ -70,16 +72,23 @@ export function TileDragProvider({
const sensors = useSensors(mouseSensor, touchSensor, keyboardSensor);
- const [dragId, setDragId] = useState();
- const [overId, setOverId] = useState();
+ const [dragId, setDragId] = useState(null);
+ const [overId, setOverId] = useState(null);
const [dragCursor, setDragCursor] = useState("pointer");
const [preventSelect, resumeSelect] = usePreventSelect();
+ const [overGroupId, setOverGroupId] = useState(null);
+ useEffect(() => {
+ setOverGroupId(
+ (overId && overId.startsWith(GROUP_ID_PREFIX) && overId.slice(9)) || null
+ );
+ }, [overId]);
+
function handleDragStart(event) {
const { active, over } = event;
setDragId(active.id);
- setOverId(over?.id);
+ setOverId(over?.id || null);
if (!selectedGroupIds.includes(active.id)) {
onGroupSelect(active.id);
}
@@ -93,7 +102,7 @@ export function TileDragProvider({
function handleDragOver(event) {
const { over } = event;
- setOverId(over?.id);
+ setOverId(over?.id || null);
if (over) {
if (
over.id.startsWith(UNGROUP_ID) ||
@@ -111,8 +120,8 @@ export function TileDragProvider({
function handleDragEnd(event) {
const { active, over, overlayNodeClientRect } = event;
- setDragId();
- setOverId();
+ setDragId(null);
+ setOverId(null);
setDragCursor("pointer");
if (active && over && active.id !== over.id) {
let selectedIndices = selectedGroupIds.map((groupId) =>
@@ -165,8 +174,8 @@ export function TileDragProvider({
}
function handleDragCancel(event) {
- setDragId();
- setOverId();
+ setDragId(null);
+ setOverId(null);
setDragCursor("pointer");
resumeSelect();
@@ -210,8 +219,6 @@ export function TileDragProvider({
return closestCenter(otherRects, rect);
}
- const value = { dragId, overId, dragCursor };
-
return (
-
- {children}
-
+
+
+
+ {children}
+
+
+
);
}
-export function useTileDrag() {
- const context = useContext(TileDragContext);
+export function useTileDragId() {
+ const context = useContext(TileDragIdContext);
if (context === undefined) {
throw new Error("useTileDrag must be used within a TileDragProvider");
}
return context;
}
-export default TileDragContext;
+export function useTileOverGroupId() {
+ const context = useContext(TileOverGroupIdContext);
+ if (context === undefined) {
+ throw new Error("useTileDrag must be used within a TileDragProvider");
+ }
+ return context;
+}
+
+export function useTileDragCursor() {
+ const context = useContext(TileDragCursorContext);
+ if (context === undefined) {
+ throw new Error("useTileDrag must be used within a TileDragProvider");
+ }
+ return context;
+}
diff --git a/src/contexts/TokenDataContext.js b/src/contexts/TokenDataContext.js
index 9c98490..72780f3 100644
--- a/src/contexts/TokenDataContext.js
+++ b/src/contexts/TokenDataContext.js
@@ -115,30 +115,24 @@ export function TokenDataProvider({ children }) {
}
function handleTokenChanges(changes) {
+ // Pool token changes together to call a single state update at the end
+ let tokensCreated = [];
+ let tokensUpdated = {};
+ let tokensDeleted = [];
for (let change of changes) {
if (change.table === "tokens") {
if (change.type === 1) {
// Created
const token = change.obj;
- setTokens((prevTokens) => [token, ...prevTokens]);
+ tokensCreated.push(token);
} else if (change.type === 2) {
// Updated
const token = change.obj;
- setTokens((prevTokens) => {
- const newTokens = [...prevTokens];
- const i = newTokens.findIndex((t) => t.id === token.id);
- if (i > -1) {
- newTokens[i] = token;
- }
- return newTokens;
- });
+ tokensUpdated[token.id] = token;
} else if (change.type === 3) {
// Deleted
const id = change.key;
- setTokens((prevTokens) => {
- const filtered = prevTokens.filter((token) => token.id !== id);
- return filtered;
- });
+ tokensDeleted.push(id);
}
}
if (change.table === "groups") {
@@ -149,6 +143,23 @@ export function TokenDataProvider({ children }) {
}
}
}
+ const tokensUpdatedArray = Object.values(tokensUpdated);
+ if (
+ tokensCreated.length > 0 ||
+ tokensUpdatedArray.length > 0 ||
+ tokensDeleted.length > 0
+ ) {
+ setTokens((prevTokens) => {
+ let newTokens = [...tokensCreated, ...prevTokens];
+ for (let token of tokensUpdatedArray) {
+ const tokenIndex = newTokens.findIndex((t) => t.id === token.id);
+ if (tokenIndex > -1) {
+ newTokens[tokenIndex] = token;
+ }
+ }
+ return newTokens.filter((token) => !tokensDeleted.includes(token.id));
+ });
+ }
}
database.on("changes", handleTokenChanges);
diff --git a/src/docs/releaseNotes/v1.9.0.md b/src/docs/releaseNotes/v1.9.0.md
index 8249418..94d3e25 100644
--- a/src/docs/releaseNotes/v1.9.0.md
+++ b/src/docs/releaseNotes/v1.9.0.md
@@ -6,7 +6,7 @@ This release focuses on improving the workflow for managing assets as well as in
All assets (tokens and maps) now offer a lot more flexibility for organisation with a new drag and drop interface for rearrangement, grouping and adding to a game.
-To access these features simply long press on any asset you have selected. You can then drag to the edge of another asset to re-order the element, drag over another asset to create a new group or drag out of the select screen to add that asset to the game.
+To access these features with mouse and keyboard simply drag any asset you have selected or with touch devices tap and hold to pickup the assets. You can then drag to the edge of another asset to re-order the element, drag over another asset to create a new group or drag out of the select screen to add that asset to the game.
With this feature we now also support the ability to drag and drop images from your computer directly into a game without the need to first add the asset into the appropriate asset import screen. To use this you can simply drag an image file into a game. Two new import boxes will then be shown where you can drag the images into the top box to import them as a map or the bottom box to import them as a token. When importing a single map this way the game will automatically switch to show this map in the shared scene. When importing tokens this way they will automatically be placed onto a shared map if one is shown.
@@ -36,6 +36,7 @@ While in edit mode we now render fog much more efficiently. In testing we have s
- The progress loading bar will now pool consecutive assets together to avoid showing each asset separately.
- All default maps and tokens are now fully customisable. This includes adjusting settings, import/export and even deleting.
+- Removed grid from default map images to allow changing grid size and type.
- Modals have a new transition animation to match the new group UI.
- Cursors now better represent drag and drop actions.
- Tokens in the select token screen now show an indicator for whether they are hidden in the sidebar.
@@ -43,3 +44,10 @@ While in edit mode we now render fog much more efficiently. In testing we have s
- Fixed a bug with the fog brush tool not working properly on maps with smaller grid sizes.
- Added better file type handling for image import screens with more informative error notifications.
- Fixed a bug with vehicle tokens not picking up tokens hidden outside of view.
+- Added a notification for when using the cut fog tool on places with no fog.
+- Added the ability for the notes text input area to automatically resize to show more lines of text.
+- Updated the light theme to be more readable in some cases.
+- Added the ability to specify token sizes in input files by using width x height syntax.
+- Updated measure tool to snap to grid cell centers to prevent issues with measuring diagonals.
+- Updated measure tool euclidean option to use pixel coordinates to be more precise.
+- Fixed a bug with note and label kerning on Firefox for Windows.
diff --git a/src/helpers/grid.js b/src/helpers/grid.js
index b7612c8..57ecd2a 100644
--- a/src/helpers/grid.js
+++ b/src/helpers/grid.js
@@ -289,10 +289,7 @@ export function gridDistance(grid, a, b, cellSize) {
const bCoord = getNearestCellCoordinates(grid, b.x, b.y, cellSize);
if (grid.type === "square") {
if (grid.measurement.type === "chebyshev") {
- return Math.max(
- Math.abs(aCoord.x - bCoord.x),
- Math.abs(aCoord.y - bCoord.y)
- );
+ return Vector2.max(Vector2.abs(Vector2.subtract(aCoord, bCoord)));
} else if (grid.measurement.type === "alternating") {
// Alternating diagonal distance like D&D 3.5 and Pathfinder
const delta = Vector2.abs(Vector2.subtract(aCoord, bCoord));
@@ -300,7 +297,7 @@ export function gridDistance(grid, a, b, cellSize) {
const min = Vector2.min(delta);
return max - min + Math.floor(1.5 * min);
} else if (grid.measurement.type === "euclidean") {
- return Vector2.distance(aCoord, bCoord);
+ return Vector2.length(Vector2.divide(Vector2.subtract(a, b), cellSize));
} else if (grid.measurement.type === "manhattan") {
return Math.abs(aCoord.x - bCoord.x) + Math.abs(aCoord.y - bCoord.y);
}
@@ -316,7 +313,7 @@ export function gridDistance(grid, a, b, cellSize) {
2
);
} else if (grid.measurement.type === "euclidean") {
- return Vector2.distance(aCoord, bCoord);
+ return Vector2.length(Vector2.divide(Vector2.subtract(a, b), cellSize));
}
}
}
diff --git a/src/helpers/image.js b/src/helpers/image.js
index 64f9765..2223741 100644
--- a/src/helpers/image.js
+++ b/src/helpers/image.js
@@ -189,41 +189,64 @@ export async function createThumbnail(image, type, size = 300, quality = 0.5) {
* @returns {Outline}
*/
export function getImageOutline(image, maxPoints = 100) {
- let baseOutline = imageOutline(image);
+ // Basic rect outline for fail conditions
+ const defaultOutline = {
+ type: "rect",
+ x: 0,
+ y: 0,
+ width: image.width,
+ height: image.height,
+ };
+ try {
+ let outlinePoints = imageOutline(image, {
+ opacityThreshold: 1, // Allow everything except full transparency
+ });
- if (baseOutline) {
- if (baseOutline.length > maxPoints) {
- baseOutline = Vector2.resample(baseOutline, maxPoints);
- }
- const bounds = Vector2.getBoundingBox(baseOutline);
- if (Vector2.rectangular(baseOutline)) {
- return {
- type: "rect",
- x: Math.round(bounds.min.x),
- y: Math.round(bounds.min.y),
- width: Math.round(bounds.width),
- height: Math.round(bounds.height),
- };
- } else if (
- Vector2.circular(
- baseOutline,
- Math.max(bounds.width / 10, bounds.height / 10)
- )
- ) {
- return {
- type: "circle",
- x: Math.round(bounds.center.x),
- y: Math.round(bounds.center.y),
- radius: Math.round(Math.min(bounds.width, bounds.height) / 2),
- };
+ if (outlinePoints) {
+ if (outlinePoints.length > maxPoints) {
+ outlinePoints = Vector2.resample(outlinePoints, maxPoints);
+ }
+ const bounds = Vector2.getBoundingBox(outlinePoints);
+
+ // Reject outline if it's area is less than 5% of the image
+ const imageArea = image.width * image.height;
+ const area = bounds.width * bounds.height;
+ if (area < imageArea * 0.05) {
+ return defaultOutline;
+ }
+
+ // Detect if the outline is a rectangle or circle
+ if (Vector2.rectangular(outlinePoints)) {
+ return {
+ type: "rect",
+ x: Math.round(bounds.min.x),
+ y: Math.round(bounds.min.y),
+ width: Math.round(bounds.width),
+ height: Math.round(bounds.height),
+ };
+ } else if (
+ Vector2.circular(
+ outlinePoints,
+ Math.max(bounds.width / 10, bounds.height / 10)
+ )
+ ) {
+ return {
+ type: "circle",
+ x: Math.round(bounds.center.x),
+ y: Math.round(bounds.center.y),
+ radius: Math.round(Math.min(bounds.width, bounds.height) / 2),
+ };
+ } else {
+ // Flatten and round outline to save on storage size
+ const points = outlinePoints
+ .map(({ x, y }) => [Math.round(x), Math.round(y)])
+ .flat();
+ return { type: "path", points };
+ }
} else {
- // Flatten and round outline to save on storage size
- const points = baseOutline
- .map(({ x, y }) => [Math.round(x), Math.round(y)])
- .flat();
- return { type: "path", points };
+ return defaultOutline;
}
- } else {
- return { type: "rect", x: 0, y: 0, width: 1, height: 1 };
+ } catch {
+ return defaultOutline;
}
}
diff --git a/src/helpers/token.js b/src/helpers/token.js
index cb88ec5..d5eefc4 100644
--- a/src/helpers/token.js
+++ b/src/helpers/token.js
@@ -39,7 +39,24 @@ export async function createTokenFromFile(file, userId) {
return Promise.reject();
}
let name = "Unknown Token";
+ let defaultSize = 1;
if (file.name) {
+ if (file.name.matchAll) {
+ // Match against a regex to find the grid size in the file name
+ // e.g. Cave 22x23 will return [["22x22", "22", "x", "23"]]
+ const sizeMatches = [...file.name.matchAll(/(\d+) ?(x|X) ?(\d+)/g)];
+ for (let match of sizeMatches) {
+ const matchX = parseInt(match[1]);
+ const matchY = parseInt(match[3]);
+ if (
+ !isNaN(matchX) &&
+ !isNaN(matchY) &&
+ matchX < 256 // Add check to test match isn't resolution
+ ) {
+ defaultSize = matchX;
+ }
+ }
+ }
// Remove file extension
name = file.name.replace(/\.[^/.]+$/, "");
// Removed grid size expression
@@ -79,6 +96,7 @@ export async function createTokenFromFile(file, userId) {
const token = {
name,
+ defaultSize,
thumbnail: thumbnail.id,
file: fileAsset.id,
id: uuid(),
@@ -86,7 +104,6 @@ export async function createTokenFromFile(file, userId) {
created: Date.now(),
lastModified: Date.now(),
owner: userId,
- defaultSize: 1,
defaultCategory: "character",
defaultLabel: "",
hideInSidebar: false,
diff --git a/src/hooks/useGridSnapping.js b/src/hooks/useGridSnapping.js
index 9c1a85c..aa52040 100644
--- a/src/hooks/useGridSnapping.js
+++ b/src/hooks/useGridSnapping.js
@@ -17,12 +17,16 @@ import {
/**
* Returns a function that when called will snap a node to the current grid
* @param {number=} snappingSensitivity 1 = Always snap, 0 = never snap if undefined the default user setting will be used
+ * @param {boolean=} useCorners Snap to grid cell corners
*/
-function useGridSnapping(snappingSensitivity) {
+function useGridSnapping(snappingSensitivity, useCorners = true) {
const [defaultSnappingSensitivity] = useSetting(
"map.gridSnappingSensitivity"
);
- snappingSensitivity = snappingSensitivity || defaultSnappingSensitivity;
+ snappingSensitivity =
+ snappingSensitivity === undefined
+ ? defaultSnappingSensitivity
+ : snappingSensitivity;
const grid = useGrid();
const gridOffset = useGridOffset();
@@ -57,7 +61,10 @@ function useGridSnapping(snappingSensitivity) {
gridCellPixelSize
);
- const snapPoints = [cellPosition, ...cellCorners];
+ const snapPoints = [cellPosition];
+ if (useCorners) {
+ snapPoints.push(...cellCorners);
+ }
for (let snapPoint of snapPoints) {
const distanceToSnapPoint = Vector2.distance(offsetPosition, snapPoint);
diff --git a/src/hooks/usePreventSelect.js b/src/hooks/usePreventSelect.js
index d20bb40..82d36bd 100644
--- a/src/hooks/usePreventSelect.js
+++ b/src/hooks/usePreventSelect.js
@@ -1,5 +1,14 @@
function usePreventSelect() {
+ function clearSelection() {
+ if (window.getSelection) {
+ window.getSelection().removeAllRanges();
+ }
+ if (document.selection) {
+ document.selection.empty();
+ }
+ }
function preventSelect() {
+ clearSelection();
document.body.classList.add("no-select");
}
diff --git a/src/index.js b/src/index.js
index d21cd5b..5e47f24 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,6 +1,7 @@
import React from "react";
import ReactDOM from "react-dom";
import * as Sentry from "@sentry/react";
+import { Dedupe } from "@sentry/integrations";
import App from "./App";
import Modal from "react-modal";
@@ -16,10 +17,16 @@ if (!("PointerEvent" in window)) {
import("pepjs");
}
+// Intersection observer polyfill
+if (!("IntersectionObserver" in window)) {
+ import("intersection-observer");
+}
+
if (process.env.REACT_APP_LOGGING === "true") {
Sentry.init({
dsn: process.env.REACT_APP_SENTRY_DSN,
release: "owlbear-rodeo@" + process.env.REACT_APP_VERSION,
+ integrations: [new Dedupe()],
// Ignore resize error as it is triggered by going fullscreen on slower computers
// Ignore quota error
// Ignore XDR encoding failure bug in Firefox https://bugzilla.mozilla.org/show_bug.cgi?id=1678243
diff --git a/src/maps/Blank Grid 22x22.jpg b/src/maps/Blank Grid 22x22.jpg
deleted file mode 100755
index 55da0c9..0000000
Binary files a/src/maps/Blank Grid 22x22.jpg and /dev/null differ
diff --git a/src/maps/Blank.jpg b/src/maps/Blank.jpg
new file mode 100755
index 0000000..ebec3c5
Binary files /dev/null and b/src/maps/Blank.jpg differ
diff --git a/src/maps/Grass Grid 22x22.jpg b/src/maps/Grass Grid 22x22.jpg
deleted file mode 100755
index 592f5f0..0000000
Binary files a/src/maps/Grass Grid 22x22.jpg and /dev/null differ
diff --git a/src/maps/Grass.jpg b/src/maps/Grass.jpg
new file mode 100755
index 0000000..0bb8a23
Binary files /dev/null and b/src/maps/Grass.jpg differ
diff --git a/src/maps/Sand Grid 22x22.jpg b/src/maps/Sand Grid 22x22.jpg
deleted file mode 100755
index 9985172..0000000
Binary files a/src/maps/Sand Grid 22x22.jpg and /dev/null differ
diff --git a/src/maps/Sand.jpg b/src/maps/Sand.jpg
new file mode 100755
index 0000000..b76b982
Binary files /dev/null and b/src/maps/Sand.jpg differ
diff --git a/src/maps/Stone Grid 22x22.jpg b/src/maps/Stone Grid 22x22.jpg
deleted file mode 100755
index 7e3be20..0000000
Binary files a/src/maps/Stone Grid 22x22.jpg and /dev/null differ
diff --git a/src/maps/Stone.jpg b/src/maps/Stone.jpg
new file mode 100755
index 0000000..f300325
Binary files /dev/null and b/src/maps/Stone.jpg differ
diff --git a/src/maps/Unknown Grid 22x22.jpg b/src/maps/Unknown.jpg
similarity index 100%
rename from src/maps/Unknown Grid 22x22.jpg
rename to src/maps/Unknown.jpg
diff --git a/src/maps/Water Grid 22x22.jpg b/src/maps/Water Grid 22x22.jpg
deleted file mode 100755
index 1525aac..0000000
Binary files a/src/maps/Water Grid 22x22.jpg and /dev/null differ
diff --git a/src/maps/Water.jpg b/src/maps/Water.jpg
new file mode 100755
index 0000000..8cceaa0
Binary files /dev/null and b/src/maps/Water.jpg differ
diff --git a/src/maps/Wood Grid 22x22.jpg b/src/maps/Wood Grid 22x22.jpg
deleted file mode 100755
index a1074f3..0000000
Binary files a/src/maps/Wood Grid 22x22.jpg and /dev/null differ
diff --git a/src/maps/Wood.jpg b/src/maps/Wood.jpg
new file mode 100755
index 0000000..9ba25be
Binary files /dev/null and b/src/maps/Wood.jpg differ
diff --git a/src/maps/index.js b/src/maps/index.js
index aca8daf..99c4192 100644
--- a/src/maps/index.js
+++ b/src/maps/index.js
@@ -1,13 +1,13 @@
import Case from "case";
-import blankImage from "./Blank Grid 22x22.jpg";
-import grassImage from "./Grass Grid 22x22.jpg";
-import sandImage from "./Sand Grid 22x22.jpg";
-import stoneImage from "./Stone Grid 22x22.jpg";
-import waterImage from "./Water Grid 22x22.jpg";
-import woodImage from "./Wood Grid 22x22.jpg";
+import blankImage from "./Blank.jpg";
+import grassImage from "./Grass.jpg";
+import sandImage from "./Sand.jpg";
+import stoneImage from "./Stone.jpg";
+import waterImage from "./Water.jpg";
+import woodImage from "./Wood.jpg";
-import unknownImage from "./Unknown Grid 22x22.jpg";
+import unknownImage from "./Unknown.jpg";
export const mapSources = {
blank: blankImage,
@@ -42,7 +42,7 @@ export function getDefaultMaps(userId) {
type: "default",
created: mapKeys.length - i,
lastModified: Date.now(),
- showGrid: false,
+ showGrid: key !== "stone",
snapToGrid: true,
group: "",
};
diff --git a/src/modals/EditMapModal.js b/src/modals/EditMapModal.js
index 4d2f6aa..4582b05 100644
--- a/src/modals/EditMapModal.js
+++ b/src/modals/EditMapModal.js
@@ -132,7 +132,7 @@ function EditMapModal({
- handleMapsChanged(e, [map])}
- />
- {map.name}
-
- );
+ if (map) {
+ return (
+
+ );
+ }
} else {
- return renderGroupContainer(
- group,
- group.items.some((item) => maps[item.id].checked),
- renderMapGroup,
- (e, group) =>
- handleMapsChanged(
- e,
- group.items.map((group) => maps[group.id])
- )
- );
+ if (group.items.some((item) => item.id in maps)) {
+ return renderGroupContainer(
+ group,
+ group.items.some((item) => maps[item.id]?.checked),
+ renderMapGroup,
+ (e, group) =>
+ handleMapsChanged(
+ e,
+ group.items
+ .filter((group) => group.id in maps)
+ .map((group) => maps[group.id])
+ )
+ );
+ }
}
}
function renderTokenGroup(group) {
if (group.type === "item") {
const token = tokens[group.id];
- return (
-
-
- {token.id in tokenUsedCount && token.type !== "default" && (
-
- Token used in {tokenUsedCount[token.id]} selected map
- {tokenUsedCount[token.id] > 1 && "s"}
-
- )}
-
- );
+ if (token) {
+ return (
+
+
+ {token.id in tokenUsedCount && token.type !== "default" && (
+
+ Token used in {tokenUsedCount[token.id]} selected map
+ {tokenUsedCount[token.id] > 1 && "s"}
+
+ )}
+
+ );
+ }
} else {
- const checked =
- group.items.some(
- (item) => !(item.id in tokenUsedCount) && tokens[item.id].checked
- ) || group.items.every((item) => item.id in tokenUsedCount);
- return renderGroupContainer(
- group,
- checked,
- renderTokenGroup,
- (e, group) =>
- handleTokensChanged(
- e,
- group.items.map((group) => tokens[group.id])
- )
- );
+ if (group.items.some((item) => item.id in tokens)) {
+ const checked =
+ group.items.some(
+ (item) => !(item.id in tokenUsedCount) && tokens[item.id]?.checked
+ ) || group.items.every((item) => item.id in tokenUsedCount);
+ return renderGroupContainer(
+ group,
+ checked,
+ renderTokenGroup,
+ (e, group) =>
+ handleTokensChanged(
+ e,
+ group.items
+ .filter((group) => group.id in tokens)
+ .map((group) => tokens[group.id])
+ )
+ );
+ }
}
}
diff --git a/src/theme.js b/src/theme.js
index 305dcbb..b3baef4 100644
--- a/src/theme.js
+++ b/src/theme.js
@@ -16,7 +16,7 @@ const theme = {
background: "hsl(10, 10%, 98%)",
primary: "hsl(260, 100%, 80%)",
secondary: "hsl(290, 100%, 80%)",
- highlight: "hsl(260, 20%, 40%)",
+ highlight: "hsl(260, 20%, 70%)",
muted: "hsla(230, 20%, 60%, 20%)",
overlay: "hsla(230, 100%, 97%, 80%)",
border: "hsla(10, 20%, 20%, 0.5)",
@@ -188,6 +188,14 @@ const theme = {
},
},
textarea: {
+ "&:focus": {
+ outlineColor: "primary",
+ },
+ "&:disabled": {
+ backgroundColor: "muted",
+ opacity: 0.5,
+ borderColor: "text",
+ },
fontFamily: "body2",
},
},
diff --git a/yarn.lock b/yarn.lock
index 39aed16..9907962 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2312,68 +2312,78 @@
estree-walker "^1.0.1"
picomatch "^2.2.2"
-"@sentry/browser@6.2.2":
- version "6.2.2"
- resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-6.2.2.tgz#4df4ad7026b269d85b63b79a75387ce5370bc705"
- integrity sha512-K5UGyEePtVPZIFMoiRafhd4Ov0M1kdozVsVKIPZrOpJyjQdPNX+fYDNL/h0nVmgOlE2S/uu4fl4mEfe/6aLShw==
+"@sentry/browser@6.3.0":
+ version "6.3.0"
+ resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-6.3.0.tgz#523430667bff290220909fb96f35706ff808fed6"
+ integrity sha512-Rse9j5XwN9n7GnfW1mNscTS4YQ0oiBNJcaSk3Mw/vQT872Wh60yKyx5wxAw5GujFZI0NgdyPlZwZ/tGQwirRxA==
dependencies:
- "@sentry/core" "6.2.2"
- "@sentry/types" "6.2.2"
- "@sentry/utils" "6.2.2"
+ "@sentry/core" "6.3.0"
+ "@sentry/types" "6.3.0"
+ "@sentry/utils" "6.3.0"
tslib "^1.9.3"
-"@sentry/core@6.2.2":
- version "6.2.2"
- resolved "https://registry.yarnpkg.com/@sentry/core/-/core-6.2.2.tgz#ec86b5769f8855f43cb58e839f81f87074ec9a3f"
- integrity sha512-qqWbvvXtymfXh7N5eEvk97MCnMURuyFIgqWdVD4MQM6yIfDCy36CyGfuQ3ViHTLZGdIfEOhLL9/f4kzf1RzqBA==
+"@sentry/core@6.3.0":
+ version "6.3.0"
+ resolved "https://registry.yarnpkg.com/@sentry/core/-/core-6.3.0.tgz#3b8db24918a00c0b77f1663fc6d9be925f66bb3e"
+ integrity sha512-voot/lJ9gRXB6bx6tVqbEbD6jOd4Sx6Rfmm6pzfpom9C0q+fjIZTatTLq8GdXj8DzxaH1MBDSwtaq/eC3NqYpA==
dependencies:
- "@sentry/hub" "6.2.2"
- "@sentry/minimal" "6.2.2"
- "@sentry/types" "6.2.2"
- "@sentry/utils" "6.2.2"
+ "@sentry/hub" "6.3.0"
+ "@sentry/minimal" "6.3.0"
+ "@sentry/types" "6.3.0"
+ "@sentry/utils" "6.3.0"
tslib "^1.9.3"
-"@sentry/hub@6.2.2":
- version "6.2.2"
- resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-6.2.2.tgz#f451d8d3ad207e81556b4846d810226693e0444e"
- integrity sha512-VR6uQGRYt6RP633FHShlSLj0LUKGVrlTeSlwCoooWM5FR9lmi6akAaweuxpG78/kZvXrAWpjX6/nuYwHKGwzGA==
+"@sentry/hub@6.3.0":
+ version "6.3.0"
+ resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-6.3.0.tgz#4225b3b0f31fe47f24d80753b257a4b57de5d651"
+ integrity sha512-lAnW3Om66t9IR+t1wya1NpOF9lGbvYG6Ca8wxJJGJ1t2PxKwyxpZKzRx0q8M1QFhlZ5cETCzxmM7lBEZ4QVCBg==
dependencies:
- "@sentry/types" "6.2.2"
- "@sentry/utils" "6.2.2"
+ "@sentry/types" "6.3.0"
+ "@sentry/utils" "6.3.0"
tslib "^1.9.3"
-"@sentry/minimal@6.2.2":
- version "6.2.2"
- resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-6.2.2.tgz#01f41e0a6a6a2becfc99f6bb6f9c4bddf54f8dae"
- integrity sha512-l0IgoGQgg1lTd4qDU8bQn25sbZBg8PwIHfuTLbGMlRr1flDXHOM1UXajWK/UKbAPelnU7M2JBSVzgl7PwjprzA==
+"@sentry/integrations@^6.3.0":
+ version "6.3.0"
+ resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-6.3.0.tgz#2091889d1d4a319d48e84ebce43405c6a7fee5b7"
+ integrity sha512-/bl0wykJr+7zJHmnAulI+/J1kT5AI/019jWSXX7nmfIhp2sRXNUw0jeNVh+xfwrbR6Ik6IleAyzwHNYKzedGVQ==
dependencies:
- "@sentry/hub" "6.2.2"
- "@sentry/types" "6.2.2"
+ "@sentry/types" "6.3.0"
+ "@sentry/utils" "6.3.0"
+ localforage "^1.8.1"
tslib "^1.9.3"
-"@sentry/react@^6.2.2":
- version "6.2.2"
- resolved "https://registry.yarnpkg.com/@sentry/react/-/react-6.2.2.tgz#6a93fa1013b2b9e37a8c0bc16cf0cbf4353de4c6"
- integrity sha512-yDuxPOD4j2WE5nX1p48GIqXwrrmwkjryFjtYvLgzGJkiGWLmGTrxrSqtUKrbqahJpKt3mi24Nkg0cMlsFB178g==
+"@sentry/minimal@6.3.0":
+ version "6.3.0"
+ resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-6.3.0.tgz#e64d87c92a4676a11168672a96589f46985f2b22"
+ integrity sha512-ZdPUwdPQkaKroy67NkwQRqmnfKyd/C1OyouM9IqYKyBjAInjOijwwc/Rd91PMHalvCOGfp1scNZYbZ+YFs/qQQ==
dependencies:
- "@sentry/browser" "6.2.2"
- "@sentry/minimal" "6.2.2"
- "@sentry/types" "6.2.2"
- "@sentry/utils" "6.2.2"
+ "@sentry/hub" "6.3.0"
+ "@sentry/types" "6.3.0"
+ tslib "^1.9.3"
+
+"@sentry/react@^6.3.0":
+ version "6.3.0"
+ resolved "https://registry.yarnpkg.com/@sentry/react/-/react-6.3.0.tgz#97763bf006991460126fa7235ebc662cd340f804"
+ integrity sha512-5+Q2p65WMxslaW96209wUp0kfqT0HTyVV+4TTCIOA6Aj3rnKesQaR44mXHXlQVTQh2/8fk1PTkMEsvWJdSPkjA==
+ dependencies:
+ "@sentry/browser" "6.3.0"
+ "@sentry/minimal" "6.3.0"
+ "@sentry/types" "6.3.0"
+ "@sentry/utils" "6.3.0"
hoist-non-react-statics "^3.3.2"
tslib "^1.9.3"
-"@sentry/types@6.2.2":
- version "6.2.2"
- resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.2.2.tgz#9fc7795156680d3da5fc6ecc66702d8f7917f2b1"
- integrity sha512-Y/1sRtw3a5JU4YdNBig8lLSVJ1UdYtuge+QP1CVLcLSAbq07Ok1bvF+Z+BlNcnHqle2Fl8aKuryG5Yu86enOyQ==
+"@sentry/types@6.3.0":
+ version "6.3.0"
+ resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.3.0.tgz#919cc1870f34b7126546c77e3c695052795d3add"
+ integrity sha512-xWyCYDmFPjS5ex60kxOOHbHEs4vs00qHbm0iShQfjl4OSg9S2azkcWofDmX8Xbn0FSOUXgdPCjNJW1B0bPVhCA==
-"@sentry/utils@6.2.2":
- version "6.2.2"
- resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.2.2.tgz#69f7151db74e65a010cec062cc9ab3e30bf2c80a"
- integrity sha512-qaee6X6VDNZ8HeO83/veaKw0KuhDE7j1R+Yryme3PywFzsoTzutDrEQjb7gvcHAhBaAYX8IHUBHgxcFI9BxI+w==
+"@sentry/utils@6.3.0":
+ version "6.3.0"
+ resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.3.0.tgz#e28645b6d4acd03a478e58bfe112ea02f81e94a0"
+ integrity sha512-NZzw4oLelgvCsVBG2e+ZtFtaBvgA7rZYtcGFbZTphhAlYoJ6JMCQUzYk0iwJK79yR1quh510x4UE0jynvvToWg==
dependencies:
- "@sentry/types" "6.2.2"
+ "@sentry/types" "6.3.0"
tslib "^1.9.3"
"@sinonjs/commons@^1.7.0":
@@ -7192,6 +7202,11 @@ image-outline@^0.1.0:
marching-squares "^0.2.0"
minimist "^1.2.0"
ndarray "^1.0.18"
+
+immediate@~3.0.5:
+ version "3.0.6"
+ resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
+ integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=
immer@8.0.1:
version "8.0.1"
@@ -7317,6 +7332,11 @@ internal-slot@^1.0.2:
has "^1.0.3"
side-channel "^1.0.2"
+intersection-observer@^0.12.0:
+ version "0.12.0"
+ resolved "https://registry.yarnpkg.com/intersection-observer/-/intersection-observer-0.12.0.tgz#6c84628f67ce8698e5f9ccf857d97718745837aa"
+ integrity sha512-2Vkz8z46Dv401zTWudDGwO7KiGHNDkMv417T5ItcNYfmvHR/1qCTVBO9vwH8zZmQ0WkA/1ARwpysR9bsnop4NQ==
+
invariant@^2.2.2:
version "2.2.4"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
@@ -8465,6 +8485,13 @@ levn@~0.3.0:
prelude-ls "~1.1.2"
type-check "~0.3.2"
+lie@3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e"
+ integrity sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=
+ dependencies:
+ immediate "~3.0.5"
+
line-column@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/line-column/-/line-column-1.0.2.tgz#d25af2936b6f4849172b312e4792d1d987bc34a2"
@@ -8527,6 +8554,13 @@ loader-utils@^1.1.0, loader-utils@^1.2.3, loader-utils@^1.4.0:
emojis-list "^3.0.0"
json5 "^1.0.1"
+localforage@^1.8.1:
+ version "1.9.0"
+ resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.9.0.tgz#f3e4d32a8300b362b4634cc4e066d9d00d2f09d1"
+ integrity sha512-rR1oyNrKulpe+VM9cYmcFn6tsHuokyVHFaCM3+osEmxaHTbEk8oQu6eGDfS6DQLWi/N67XRmB8ECG37OES368g==
+ dependencies:
+ lie "3.1.1"
+
locate-path@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
@@ -10857,6 +10891,11 @@ react-input-autosize@^3.0.0:
dependencies:
prop-types "^15.5.8"
+react-intersection-observer@^8.32.0:
+ version "8.32.0"
+ resolved "https://registry.yarnpkg.com/react-intersection-observer/-/react-intersection-observer-8.32.0.tgz#47249332e12e8bb99ed35a10bb7dd10446445a7b"
+ integrity sha512-RlC6FvS3MFShxTn4FHAy904bVjX5Nn4/eTjUkurW0fHK+M/fyQdXuyCy9+L7yjA+YMGogzzSJNc7M4UtfSKvtw==
+
react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.6:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
@@ -11062,6 +11101,15 @@ react-spring@^8.0.27:
"@babel/runtime" "^7.3.1"
prop-types "^15.5.8"
+react-textarea-autosize@^8.3.3:
+ version "8.3.3"
+ resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-8.3.3.tgz#f70913945369da453fd554c168f6baacd1fa04d8"
+ integrity sha512-2XlHXK2TDxS6vbQaoPbMOfQ8GK7+irc2fVK6QFIcC8GOnH3zI/v481n+j1L0WaPVvKxwesnY93fEfH++sus2rQ==
+ dependencies:
+ "@babel/runtime" "^7.10.2"
+ use-composed-ref "^1.0.0"
+ use-latest "^1.0.0"
+
react-toast-notifications@^2.4.3:
version "2.4.3"
resolved "https://registry.yarnpkg.com/react-toast-notifications/-/react-toast-notifications-2.4.3.tgz#ebf2ee776615a97906cef214352cfd9fe800c583"
@@ -12816,6 +12864,11 @@ tryer@^1.0.1:
resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8"
integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==
+ts-essentials@^2.0.3:
+ version "2.0.12"
+ resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-2.0.12.tgz#c9303f3d74f75fa7528c3d49b80e089ab09d8745"
+ integrity sha512-3IVX4nI6B5cc31/GFFE+i8ey/N2eA0CZDbo6n0yrz0zDX8ZJ8djmU1p+XRz7G3is0F3bB3pu2pAroFdAWQKU3w==
+
ts-pnp@1.2.0, ts-pnp@^1.1.6:
version "1.2.0"
resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92"
@@ -13156,11 +13209,30 @@ url@^0.11.0:
punycode "1.3.2"
querystring "0.2.0"
+use-composed-ref@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/use-composed-ref/-/use-composed-ref-1.1.0.tgz#9220e4e94a97b7b02d7d27eaeab0b37034438bbc"
+ integrity sha512-my1lNHGWsSDAhhVAT4MKs6IjBUtG6ZG11uUqexPH9PptiIZDQOzaF4f5tEbJ2+7qvNbtXNBbU3SfmN+fXlWDhg==
+ dependencies:
+ ts-essentials "^2.0.3"
+
use-image@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/use-image/-/use-image-1.0.7.tgz#b2c42b78d32868762a37631673eb169a244d5ad1"
integrity sha512-Z8hB8+lAGe25iqO3YaHO7bSvBSGErEakYQ6RGyRrPZoMDLKBIuZ67ikzn8f5ydjWorqFzeX+U3vVwnXoE1Q56Q==
+use-isomorphic-layout-effect@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.1.tgz#7bb6589170cd2987a152042f9084f9effb75c225"
+ integrity sha512-L7Evj8FGcwo/wpbv/qvSfrkHFtOpCzvM5yl2KVyDJoylVuSvzphiiasmjgQPttIGBAy2WKiBNR98q8w7PiNgKQ==
+
+use-latest@^1.0.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/use-latest/-/use-latest-1.2.0.tgz#a44f6572b8288e0972ec411bdd0840ada366f232"
+ integrity sha512-d2TEuG6nSLKQLAfW3By8mKr8HurOlTkul0sOpxbClIv4SQ4iOd7BYr7VIzdbktUCnv7dua/60xzd8igMU6jmyw==
+ dependencies:
+ use-isomorphic-layout-effect "^1.0.0"
+
use@^3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"