Merge branch 'test/v1.9.0' into preview/1.9.0
@ -4,6 +4,6 @@ REACT_APP_STRIPE_API_KEY=pk_live_MJjzi5djj524Y7h3fL5PNh4e00a852XD51
|
|||||||
REACT_APP_STRIPE_URL=https://payment.owlbear.rodeo
|
REACT_APP_STRIPE_URL=https://payment.owlbear.rodeo
|
||||||
REACT_APP_VERSION=$npm_package_version
|
REACT_APP_VERSION=$npm_package_version
|
||||||
REACT_APP_PREVIEW=false
|
REACT_APP_PREVIEW=false
|
||||||
REACT_APP_LOGGING=false
|
REACT_APP_LOGGING=true
|
||||||
REACT_APP_FATHOM_SITE_ID=VMSHBPKD
|
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
|
||||||
|
@ -9,7 +9,8 @@
|
|||||||
"@dnd-kit/sortable": "^3.1.0",
|
"@dnd-kit/sortable": "^3.1.0",
|
||||||
"@mitchemmc/dexie-export-import": "^1.0.1",
|
"@mitchemmc/dexie-export-import": "^1.0.1",
|
||||||
"@msgpack/msgpack": "^2.4.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",
|
"@stripe/stripe-js": "^1.13.1",
|
||||||
"@tensorflow/tfjs": "^3.3.0",
|
"@tensorflow/tfjs": "^3.3.0",
|
||||||
"@testing-library/jest-dom": "^5.11.9",
|
"@testing-library/jest-dom": "^5.11.9",
|
||||||
@ -27,6 +28,7 @@
|
|||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"fuse.js": "^6.4.6",
|
"fuse.js": "^6.4.6",
|
||||||
"image-outline": "^0.1.0",
|
"image-outline": "^0.1.0",
|
||||||
|
"intersection-observer": "^0.12.0",
|
||||||
"konva": "^7.2.5",
|
"konva": "^7.2.5",
|
||||||
"lodash.clonedeep": "^4.5.0",
|
"lodash.clonedeep": "^4.5.0",
|
||||||
"lodash.get": "^4.4.2",
|
"lodash.get": "^4.4.2",
|
||||||
@ -39,6 +41,7 @@
|
|||||||
"raw.macro": "^0.4.2",
|
"raw.macro": "^0.4.2",
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.1",
|
||||||
|
"react-intersection-observer": "^8.32.0",
|
||||||
"react-konva": "^17.0.1-3",
|
"react-konva": "^17.0.1-3",
|
||||||
"react-markdown": "4",
|
"react-markdown": "4",
|
||||||
"react-media": "^2.0.0-rc.1",
|
"react-media": "^2.0.0-rc.1",
|
||||||
@ -49,6 +52,7 @@
|
|||||||
"react-scripts": "^4.0.3",
|
"react-scripts": "^4.0.3",
|
||||||
"react-select": "^4.2.1",
|
"react-select": "^4.2.1",
|
||||||
"react-spring": "^8.0.27",
|
"react-spring": "^8.0.27",
|
||||||
|
"react-textarea-autosize": "^8.3.3",
|
||||||
"react-toast-notifications": "^2.4.3",
|
"react-toast-notifications": "^2.4.3",
|
||||||
"react-use-gesture": "^9.1.3",
|
"react-use-gesture": "^9.1.3",
|
||||||
"shortid": "^2.2.15",
|
"shortid": "^2.2.15",
|
||||||
|
@ -67,6 +67,7 @@ function Select({ creatable, ...props }) {
|
|||||||
primary25: theme.colors.highlight,
|
primary25: theme.colors.highlight,
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
|
captureMenuScroll={false}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
22
src/components/TextareaAutoSize.css
Normal file
@ -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;
|
||||||
|
}
|
8
src/components/TextareaAutoSize.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import TextareaAutosize from "react-textarea-autosize";
|
||||||
|
import "./TextareaAutoSize.css";
|
||||||
|
|
||||||
|
function StyledTextareaAutoSize(props) {
|
||||||
|
return <TextareaAutosize className="textarea-auto-size" {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StyledTextareaAutoSize;
|
@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useRef } from "react";
|
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 { useToasts } from "react-toast-notifications";
|
||||||
|
|
||||||
import LoadingOverlay from "../LoadingOverlay";
|
import LoadingOverlay from "../LoadingOverlay";
|
||||||
@ -171,39 +171,61 @@ function GlobalImageDrop({ children, onMapChange, onMapTokensStateCreate }) {
|
|||||||
{...overlayListeners}
|
{...overlayListeners}
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
bg="overlay"
|
|
||||||
sx={{
|
sx={{
|
||||||
height: "10%",
|
height: "10%",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
color: droppingType === "maps" ? "primary" : "text",
|
color: droppingType === "maps" ? "primary" : "text",
|
||||||
opacity: droppingType === "maps" ? 1 : 0.8,
|
opacity: droppingType === "maps" ? 1 : 0.8,
|
||||||
margin: "4px 16px",
|
width: "100%",
|
||||||
border: "1px dashed",
|
position: "relative",
|
||||||
width: "calc(100% - 32px)",
|
|
||||||
borderRadius: "12px",
|
|
||||||
}}
|
}}
|
||||||
onDragEnter={handleMapsOver}
|
onDragEnter={handleMapsOver}
|
||||||
>
|
>
|
||||||
|
<Box
|
||||||
|
bg="overlay"
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
margin: "4px 16px",
|
||||||
|
border: "1px dashed",
|
||||||
|
borderRadius: "12px",
|
||||||
|
pointerEvents: "none",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Text sx={{ pointerEvents: "none", userSelect: "none" }}>
|
<Text sx={{ pointerEvents: "none", userSelect: "none" }}>
|
||||||
Drop as map
|
Drop as map
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex
|
<Flex
|
||||||
bg="overlay"
|
|
||||||
sx={{
|
sx={{
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
color: droppingType === "tokens" ? "primary" : "text",
|
color: droppingType === "tokens" ? "primary" : "text",
|
||||||
opacity: droppingType === "tokens" ? 1 : 0.8,
|
opacity: droppingType === "tokens" ? 1 : 0.8,
|
||||||
margin: "4px 16px",
|
width: "100%",
|
||||||
border: "1px dashed",
|
position: "relative",
|
||||||
width: "calc(100% - 32px)",
|
|
||||||
borderRadius: "12px",
|
|
||||||
}}
|
}}
|
||||||
onDragEnter={handleTokensOver}
|
onDragEnter={handleTokensOver}
|
||||||
>
|
>
|
||||||
|
<Box
|
||||||
|
bg="overlay"
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
margin: "4px 16px",
|
||||||
|
border: "1px dashed",
|
||||||
|
borderRadius: "12px",
|
||||||
|
pointerEvents: "none",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Text sx={{ pointerEvents: "none", userSelect: "none" }}>
|
<Text sx={{ pointerEvents: "none", userSelect: "none" }}>
|
||||||
Drop as token
|
Drop as token
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Box } from "theme-ui";
|
import { Box } from "theme-ui";
|
||||||
|
import { useToasts } from "react-toast-notifications";
|
||||||
|
|
||||||
import MapControls from "./MapControls";
|
import MapControls from "./MapControls";
|
||||||
import MapInteraction from "./MapInteraction";
|
import MapInteraction from "./MapInteraction";
|
||||||
@ -49,6 +50,8 @@ function Map({
|
|||||||
disabledTokens,
|
disabledTokens,
|
||||||
session,
|
session,
|
||||||
}) {
|
}) {
|
||||||
|
const { addToast } = useToasts();
|
||||||
|
|
||||||
const { tokensById } = useTokenData();
|
const { tokensById } = useTokenData();
|
||||||
|
|
||||||
const [selectedToolId, setSelectedToolId] = useState("move");
|
const [selectedToolId, setSelectedToolId] = useState("move");
|
||||||
@ -232,6 +235,7 @@ function Map({
|
|||||||
onShapesCut={handleFogShapesCut}
|
onShapesCut={handleFogShapesCut}
|
||||||
onShapesRemove={handleFogShapesRemove}
|
onShapesRemove={handleFogShapesRemove}
|
||||||
onShapesEdit={handleFogShapesEdit}
|
onShapesEdit={handleFogShapesEdit}
|
||||||
|
onShapeError={addToast}
|
||||||
active={selectedToolId === "fog"}
|
active={selectedToolId === "fog"}
|
||||||
toolSettings={settings.fog}
|
toolSettings={settings.fog}
|
||||||
editable={allowFogDrawing && !settings.fog.preview}
|
editable={allowFogDrawing && !settings.fog.preview}
|
||||||
|
@ -38,8 +38,10 @@ import {
|
|||||||
Tick,
|
Tick,
|
||||||
getRelativePointerPosition,
|
getRelativePointerPosition,
|
||||||
} from "../../helpers/konva";
|
} from "../../helpers/konva";
|
||||||
|
import { keyBy } from "../../helpers/shared";
|
||||||
|
|
||||||
import SubtractShapeAction from "../../actions/SubtractShapeAction";
|
import SubtractShapeAction from "../../actions/SubtractShapeAction";
|
||||||
|
import CutShapeAction from "../../actions/CutShapeAction";
|
||||||
|
|
||||||
import useSetting from "../../hooks/useSetting";
|
import useSetting from "../../hooks/useSetting";
|
||||||
|
|
||||||
@ -52,6 +54,7 @@ function MapFog({
|
|||||||
onShapesCut,
|
onShapesCut,
|
||||||
onShapesRemove,
|
onShapesRemove,
|
||||||
onShapesEdit,
|
onShapesEdit,
|
||||||
|
onShapeError,
|
||||||
active,
|
active,
|
||||||
toolSettings,
|
toolSettings,
|
||||||
editable,
|
editable,
|
||||||
@ -214,6 +217,8 @@ function MapFog({
|
|||||||
) {
|
) {
|
||||||
const cut = toolSettings.useFogCut;
|
const cut = toolSettings.useFogCut;
|
||||||
let drawingShapes = [drawingShape];
|
let drawingShapes = [drawingShape];
|
||||||
|
|
||||||
|
// Filter out hidden or visible shapes if single layer enabled
|
||||||
if (!toolSettings.multilayer) {
|
if (!toolSettings.multilayer) {
|
||||||
const shapesToSubtract = shapes.filter((shape) =>
|
const shapesToSubtract = shapes.filter((shape) =>
|
||||||
cut ? !shape.visible : shape.visible
|
cut ? !shape.visible : shape.visible
|
||||||
@ -228,22 +233,32 @@ function MapFog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (drawingShapes.length > 0) {
|
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) {
|
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 {
|
} else {
|
||||||
onShapesAdd(drawingShapes);
|
onShapesAdd(
|
||||||
|
drawingShapes.map((shape) => ({ ...shape, color: "black" }))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (cut) {
|
||||||
|
onShapeError("Fog already cut");
|
||||||
|
} else {
|
||||||
|
onShapeError("Fog already placed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setDrawingShape(null);
|
setDrawingShape(null);
|
||||||
@ -373,6 +388,7 @@ function MapFog({
|
|||||||
};
|
};
|
||||||
|
|
||||||
let polygonShapes = [polygonShape];
|
let polygonShapes = [polygonShape];
|
||||||
|
// Filter out hidden or visible shapes if single layer enabled
|
||||||
if (!toolSettings.multilayer) {
|
if (!toolSettings.multilayer) {
|
||||||
const shapesToSubtract = shapes.filter((shape) =>
|
const shapesToSubtract = shapes.filter((shape) =>
|
||||||
cut ? !shape.visible : shape.visible
|
cut ? !shape.visible : shape.visible
|
||||||
@ -388,7 +404,15 @@ function MapFog({
|
|||||||
|
|
||||||
if (polygonShapes.length > 0) {
|
if (polygonShapes.length > 0) {
|
||||||
if (cut) {
|
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 {
|
} else {
|
||||||
onShapesAdd(
|
onShapesAdd(
|
||||||
polygonShapes.map((shape) => ({
|
polygonShapes.map((shape) => ({
|
||||||
@ -399,10 +423,23 @@ function MapFog({
|
|||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (cut) {
|
||||||
|
onShapeError("Fog already cut");
|
||||||
|
} else {
|
||||||
|
onShapeError("Fog already placed");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setDrawingShape(null);
|
setDrawingShape(null);
|
||||||
}, [toolSettings, drawingShape, onShapesCut, onShapesAdd, shapes]);
|
}, [
|
||||||
|
toolSettings,
|
||||||
|
drawingShape,
|
||||||
|
onShapesCut,
|
||||||
|
onShapesAdd,
|
||||||
|
onShapeError,
|
||||||
|
shapes,
|
||||||
|
]);
|
||||||
|
|
||||||
// Add keyboard shortcuts
|
// Add keyboard shortcuts
|
||||||
function handleKeyDown(event) {
|
function handleKeyDown(event) {
|
||||||
|
@ -44,7 +44,10 @@ function MapMeasure({ map, active }) {
|
|||||||
|
|
||||||
const gridScale = parseGridScale(active && grid.measurement.scale);
|
const gridScale = parseGridScale(active && grid.measurement.scale);
|
||||||
|
|
||||||
const snapPositionToGrid = useGridSnapping();
|
const snapPositionToGrid = useGridSnapping(
|
||||||
|
grid.measurement.type === "euclidean" ? 0 : 1,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!active) {
|
if (!active) {
|
||||||
|
@ -4,6 +4,7 @@ import MapTile from "./MapTile";
|
|||||||
import MapTileGroup from "./MapTileGroup";
|
import MapTileGroup from "./MapTileGroup";
|
||||||
|
|
||||||
import SortableTiles from "../tile/SortableTiles";
|
import SortableTiles from "../tile/SortableTiles";
|
||||||
|
import SortableTilesDragOverlay from "../tile/SortableTilesDragOverlay";
|
||||||
|
|
||||||
import { getGroupItems } from "../../helpers/group";
|
import { getGroupItems } from "../../helpers/group";
|
||||||
|
|
||||||
@ -20,21 +21,25 @@ function MapTiles({ mapsById, onMapEdit, onMapSelect, subgroup }) {
|
|||||||
function renderTile(group) {
|
function renderTile(group) {
|
||||||
if (group.type === "item") {
|
if (group.type === "item") {
|
||||||
const map = mapsById[group.id];
|
const map = mapsById[group.id];
|
||||||
const isSelected = selectedGroupIds.includes(group.id);
|
if (map) {
|
||||||
const canEdit =
|
const isSelected = selectedGroupIds.includes(group.id);
|
||||||
isSelected && selectMode === "single" && selectedGroupIds.length === 1;
|
const canEdit =
|
||||||
return (
|
isSelected &&
|
||||||
<MapTile
|
selectMode === "single" &&
|
||||||
key={map.id}
|
selectedGroupIds.length === 1;
|
||||||
map={map}
|
return (
|
||||||
isSelected={isSelected}
|
<MapTile
|
||||||
onSelect={onGroupSelect}
|
key={map.id}
|
||||||
onEdit={onMapEdit}
|
map={map}
|
||||||
onDoubleClick={() => canEdit && onMapSelect(group.id)}
|
isSelected={isSelected}
|
||||||
canEdit={canEdit}
|
onSelect={onGroupSelect}
|
||||||
badges={[`${map.grid.size.x}x${map.grid.size.y}`]}
|
onEdit={onMapEdit}
|
||||||
/>
|
onDoubleClick={() => canEdit && onMapSelect(group.id)}
|
||||||
);
|
canEdit={canEdit}
|
||||||
|
badges={[`${map.grid.size.x}x${map.grid.size.y}`]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const isSelected = selectedGroupIds.includes(group.id);
|
const isSelected = selectedGroupIds.includes(group.id);
|
||||||
const items = getGroupItems(group);
|
const items = getGroupItems(group);
|
||||||
@ -53,7 +58,12 @@ function MapTiles({ mapsById, onMapEdit, onMapSelect, subgroup }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return <SortableTiles renderTile={renderTile} subgroup={subgroup} />;
|
return (
|
||||||
|
<>
|
||||||
|
<SortableTiles renderTile={renderTile} subgroup={subgroup} />
|
||||||
|
<SortableTilesDragOverlay renderTile={renderTile} subgroup={subgroup} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MapTiles;
|
export default MapTiles;
|
||||||
|
@ -248,16 +248,20 @@ function MapToken({
|
|||||||
hitFunc={() => {}}
|
hitFunc={() => {}}
|
||||||
/>
|
/>
|
||||||
<Group offsetX={tokenWidth / 2} offsetY={tokenHeight / 2}>
|
<Group offsetX={tokenWidth / 2} offsetY={tokenHeight / 2}>
|
||||||
<TokenStatus
|
{tokenState.statuses?.length > 0 ? (
|
||||||
tokenState={tokenState}
|
<TokenStatus
|
||||||
width={tokenWidth}
|
tokenState={tokenState}
|
||||||
height={tokenHeight}
|
width={tokenWidth}
|
||||||
/>
|
height={tokenHeight}
|
||||||
<TokenLabel
|
/>
|
||||||
tokenState={tokenState}
|
) : null}
|
||||||
width={tokenWidth}
|
{tokenState.label ? (
|
||||||
height={tokenHeight}
|
<TokenLabel
|
||||||
/>
|
tokenState={tokenState}
|
||||||
|
width={tokenWidth}
|
||||||
|
height={tokenHeight}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</Group>
|
</Group>
|
||||||
</animated.Group>
|
</animated.Group>
|
||||||
);
|
);
|
||||||
|
@ -15,7 +15,7 @@ import colors from "../../helpers/colors";
|
|||||||
import usePrevious from "../../hooks/usePrevious";
|
import usePrevious from "../../hooks/usePrevious";
|
||||||
import useGridSnapping from "../../hooks/useGridSnapping";
|
import useGridSnapping from "../../hooks/useGridSnapping";
|
||||||
|
|
||||||
const minTextSize = 16;
|
const defaultFontSize = 16;
|
||||||
|
|
||||||
function Note({
|
function Note({
|
||||||
note,
|
note,
|
||||||
@ -118,7 +118,7 @@ function Note({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [fontSize, setFontSize] = useState(1);
|
const [fontScale, setFontScale] = useState(1);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const text = textRef.current;
|
const text = textRef.current;
|
||||||
|
|
||||||
@ -127,10 +127,10 @@ function Note({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function findFontSize() {
|
function findFontSize() {
|
||||||
// Create an array from 1 / minTextSize of the note height to the full note height
|
// Create an array from 1 / defaultFontSize of the note height to the full note height
|
||||||
const sizes = Array.from(
|
let sizes = Array.from(
|
||||||
{ length: Math.ceil(noteHeight - notePadding * 2) },
|
{ length: Math.ceil(noteHeight - notePadding * 2) },
|
||||||
(_, i) => i + Math.ceil(noteHeight / minTextSize)
|
(_, i) => i + Math.ceil(noteHeight / defaultFontSize)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (sizes.length > 0) {
|
if (sizes.length > 0) {
|
||||||
@ -144,8 +144,7 @@ function Note({
|
|||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
setFontScale(size / defaultFontSize);
|
||||||
setFontSize(size);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,11 +214,14 @@ function Note({
|
|||||||
}
|
}
|
||||||
align="left"
|
align="left"
|
||||||
verticalAlign="middle"
|
verticalAlign="middle"
|
||||||
padding={notePadding}
|
padding={notePadding / fontScale}
|
||||||
fontSize={fontSize}
|
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"
|
wrap="word"
|
||||||
width={noteWidth}
|
|
||||||
height={note.textOnly ? undefined : noteHeight}
|
|
||||||
/>
|
/>
|
||||||
{/* Use an invisible text block to work out text sizing */}
|
{/* Use an invisible text block to work out text sizing */}
|
||||||
<Text visible={false} ref={textRef} text={note.text} wrap="none" />
|
<Text visible={false} ref={textRef} text={note.text} wrap="none" />
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
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 Slider from "../Slider";
|
||||||
|
import TextareaAutosize from "../TextareaAutoSize";
|
||||||
|
|
||||||
import MapMenu from "../map/MapMenu";
|
import MapMenu from "../map/MapMenu";
|
||||||
|
|
||||||
@ -128,20 +129,12 @@ function NoteMenu({
|
|||||||
}}
|
}}
|
||||||
sx={{ alignItems: "center" }}
|
sx={{ alignItems: "center" }}
|
||||||
>
|
>
|
||||||
<Textarea
|
<TextareaAutosize
|
||||||
id="changeNoteText"
|
id="changeNoteText"
|
||||||
onChange={handleTextChange}
|
onChange={handleTextChange}
|
||||||
value={(note && note.text) || ""}
|
value={(note && note.text) || ""}
|
||||||
sx={{
|
|
||||||
padding: "4px",
|
|
||||||
border: "none",
|
|
||||||
":focus": {
|
|
||||||
outline: "none",
|
|
||||||
},
|
|
||||||
resize: "none",
|
|
||||||
}}
|
|
||||||
rows={1}
|
|
||||||
onKeyPress={handleTextKeyPress}
|
onKeyPress={handleTextKeyPress}
|
||||||
|
maxRows={4}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Box
|
<Box
|
||||||
|
33
src/components/tile/LazyTile.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Box } from "theme-ui";
|
||||||
|
import { useInView } from "react-intersection-observer";
|
||||||
|
|
||||||
|
function LazyTile({ children }) {
|
||||||
|
const [ref, inView] = useInView({ triggerOnce: false });
|
||||||
|
|
||||||
|
const sx = inView
|
||||||
|
? {}
|
||||||
|
: { width: "100%", height: "0", paddingTop: "100%", position: "relative" };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={sx} ref={ref}>
|
||||||
|
{inView ? (
|
||||||
|
children
|
||||||
|
) : (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
borderRadius: "4px",
|
||||||
|
}}
|
||||||
|
bg="background"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LazyTile;
|
@ -1,26 +1,25 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { createPortal } from "react-dom";
|
|
||||||
import { DragOverlay } from "@dnd-kit/core";
|
|
||||||
import { SortableContext } from "@dnd-kit/sortable";
|
import { SortableContext } from "@dnd-kit/sortable";
|
||||||
import { animated, useSpring, config } from "react-spring";
|
|
||||||
import { Badge } from "theme-ui";
|
|
||||||
|
|
||||||
import { moveGroupsInto } from "../../helpers/group";
|
import { moveGroupsInto } from "../../helpers/group";
|
||||||
import { keyBy } from "../../helpers/shared";
|
import { keyBy } from "../../helpers/shared";
|
||||||
import Vector2 from "../../helpers/Vector2";
|
|
||||||
|
|
||||||
import SortableTile from "./SortableTile";
|
import SortableTile from "./SortableTile";
|
||||||
|
import LazyTile from "./LazyTile";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useTileDrag,
|
useTileDragId,
|
||||||
|
useTileDragCursor,
|
||||||
|
useTileOverGroupId,
|
||||||
BASE_SORTABLE_ID,
|
BASE_SORTABLE_ID,
|
||||||
GROUP_SORTABLE_ID,
|
GROUP_SORTABLE_ID,
|
||||||
GROUP_ID_PREFIX,
|
|
||||||
} from "../../contexts/TileDragContext";
|
} from "../../contexts/TileDragContext";
|
||||||
import { useGroup } from "../../contexts/GroupContext";
|
import { useGroup } from "../../contexts/GroupContext";
|
||||||
|
|
||||||
function SortableTiles({ renderTile, subgroup }) {
|
function SortableTiles({ renderTile, subgroup }) {
|
||||||
const { dragId, overId, dragCursor } = useTileDrag();
|
const dragId = useTileDragId();
|
||||||
|
const dragCursor = useTileDragCursor();
|
||||||
|
const overGroupId = useTileOverGroupId();
|
||||||
const {
|
const {
|
||||||
groups,
|
groups,
|
||||||
selectedGroupIds: allSelectedIds,
|
selectedGroupIds: allSelectedIds,
|
||||||
@ -46,15 +45,6 @@ function SortableTiles({ renderTile, subgroup }) {
|
|||||||
const disableSorting = (openGroupId && !subgroup) || filter;
|
const disableSorting = (openGroupId && !subgroup) || filter;
|
||||||
const disableGrouping = subgroup || disableSorting || 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) {
|
function renderSortableGroup(group, selectedGroups) {
|
||||||
if (overGroupId === group.id && dragId && group.id !== dragId) {
|
if (overGroupId === group.id && dragId && group.id !== dragId) {
|
||||||
// If dragging over a group render a preview of that group
|
// If dragging over a group render a preview of that group
|
||||||
@ -68,57 +58,6 @@ function SortableTiles({ renderTile, subgroup }) {
|
|||||||
return renderTile(group);
|
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) => (
|
|
||||||
<DragOverlay dropAnimation={null} key={group.id}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
transform: `translate(${coords[index].x}%, ${coords[index].y}%)`,
|
|
||||||
cursor: dragCursor,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<animated.div style={dragBounce}>
|
|
||||||
{renderTile(group)}
|
|
||||||
{index === selectedIndices.length - 1 &&
|
|
||||||
selectedGroupIds.length > 1 && (
|
|
||||||
<Badge
|
|
||||||
sx={{
|
|
||||||
position: "absolute",
|
|
||||||
top: 0,
|
|
||||||
right: 0,
|
|
||||||
transform: "translate(25%, -25%)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{selectedGroupIds.length}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</animated.div>
|
|
||||||
</div>
|
|
||||||
</DragOverlay>
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderTiles() {
|
function renderTiles() {
|
||||||
const groupsByIds = keyBy(activeGroups, "id");
|
const groupsByIds = keyBy(activeGroups, "id");
|
||||||
const selectedGroupIdsSet = new Set(selectedGroupIds);
|
const selectedGroupIdsSet = new Set(selectedGroupIds);
|
||||||
@ -138,17 +77,18 @@ function SortableTiles({ renderTile, subgroup }) {
|
|||||||
const disableTileGrouping =
|
const disableTileGrouping =
|
||||||
disableGrouping || isDragging || hasSelectedContainerGroup;
|
disableGrouping || isDragging || hasSelectedContainerGroup;
|
||||||
return (
|
return (
|
||||||
<SortableTile
|
<LazyTile key={group.id}>
|
||||||
id={group.id}
|
<SortableTile
|
||||||
key={group.id}
|
id={group.id}
|
||||||
disableGrouping={disableTileGrouping}
|
disableGrouping={disableTileGrouping}
|
||||||
disableSorting={disableSorting}
|
disableSorting={disableSorting}
|
||||||
hidden={group.id === openGroupId}
|
hidden={group.id === openGroupId}
|
||||||
isDragging={isDragging}
|
isDragging={isDragging}
|
||||||
cursor={dragCursor}
|
cursor={dragCursor}
|
||||||
>
|
>
|
||||||
{renderSortableGroup(group, selectedGroups)}
|
{renderSortableGroup(group, selectedGroups)}
|
||||||
</SortableTile>
|
</SortableTile>
|
||||||
|
</LazyTile>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -156,7 +96,6 @@ function SortableTiles({ renderTile, subgroup }) {
|
|||||||
return (
|
return (
|
||||||
<SortableContext items={activeGroups} id={sortableId}>
|
<SortableContext items={activeGroups} id={sortableId}>
|
||||||
{renderTiles()}
|
{renderTiles()}
|
||||||
{createPortal(dragId && renderDragOverlays(), document.body)}
|
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
93
src/components/tile/SortableTilesDragOverlay.js
Normal file
@ -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) => (
|
||||||
|
<DragOverlay dropAnimation={null} key={group.id}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
transform: `translate(${coords[index].x}%, ${coords[index].y}%)`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<animated.div style={dragBounce}>
|
||||||
|
{renderTile(group)}
|
||||||
|
{index === selectedIndices.length - 1 &&
|
||||||
|
selectedGroupIds.length > 1 && (
|
||||||
|
<Badge
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
transform: "translate(25%, -25%)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{selectedGroupIds.length}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</animated.div>
|
||||||
|
</div>
|
||||||
|
</DragOverlay>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return createPortal(dragId && renderDragOverlays(), document.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SortableTilesDragOverlay;
|
@ -15,15 +15,13 @@ function Tile({
|
|||||||
children,
|
children,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
position: "relative",
|
position: "relative",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "0",
|
height: "0",
|
||||||
paddingTop: "100%",
|
paddingTop: "100%",
|
||||||
borderRadius: "4px",
|
borderRadius: "4px",
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
userSelect: "none",
|
userSelect: "none",
|
||||||
}}
|
}}
|
||||||
@ -120,7 +118,7 @@ function Tile({
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { Box, Flex } from "theme-ui";
|
import { Box, Flex, Grid } from "theme-ui";
|
||||||
import SimpleBar from "simplebar-react";
|
import SimpleBar from "simplebar-react";
|
||||||
import {
|
import {
|
||||||
DragOverlay,
|
DragOverlay,
|
||||||
@ -165,9 +165,16 @@ function TokenBar({ onMapTokensStateCreate }) {
|
|||||||
padding: "0 16px",
|
padding: "0 16px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex sx={{ flexDirection: "column" }}>
|
<Grid
|
||||||
|
columns="1fr"
|
||||||
|
gap={2}
|
||||||
|
py={2}
|
||||||
|
// Prevent selection on 3D touch for iOS
|
||||||
|
onTouchStart={preventSelect}
|
||||||
|
onTouchEnd={resumeSelect}
|
||||||
|
>
|
||||||
{tokenGroups.map((group) => renderToken(group))}
|
{tokenGroups.map((group) => renderToken(group))}
|
||||||
</Flex>
|
</Grid>
|
||||||
</SimpleBar>
|
</SimpleBar>
|
||||||
<Flex
|
<Flex
|
||||||
bg="muted"
|
bg="muted"
|
||||||
|
@ -1,31 +1,29 @@
|
|||||||
import React, { useRef } from "react";
|
import React from "react";
|
||||||
import { Box } from "theme-ui";
|
import { Box } from "theme-ui";
|
||||||
|
import { useInView } from "react-intersection-observer";
|
||||||
import usePreventTouch from "../../hooks/usePreventTouch";
|
|
||||||
|
|
||||||
import TokenImage from "./TokenImage";
|
import TokenImage from "./TokenImage";
|
||||||
|
|
||||||
function TokenBarToken({ token }) {
|
function TokenBarToken({ token }) {
|
||||||
const imageRef = useRef();
|
const [ref, inView] = useInView({ triggerOnce: true });
|
||||||
// Stop touch to prevent 3d touch gesutre on iOS
|
|
||||||
usePreventTouch(imageRef);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box py={1} sx={{ width: "48px", height: "56px" }}>
|
<Box ref={ref} sx={{ width: "48px", height: "48px" }} title={token.name}>
|
||||||
<TokenImage
|
{inView && (
|
||||||
token={token}
|
<TokenImage
|
||||||
ref={imageRef}
|
token={token}
|
||||||
sx={{
|
sx={{
|
||||||
userSelect: "none",
|
userSelect: "none",
|
||||||
touchAction: "none",
|
touchAction: "none",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
objectFit: "cover",
|
objectFit: "cover",
|
||||||
pointerEvents: "none",
|
pointerEvents: "none",
|
||||||
}}
|
}}
|
||||||
alt={token.name}
|
alt={token.name}
|
||||||
title={token.name}
|
title={token.name}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -40,11 +40,10 @@ function TokenBarTokenGroup({ group, tokens, draggable }) {
|
|||||||
return (
|
return (
|
||||||
<Grid
|
<Grid
|
||||||
columns="1fr"
|
columns="1fr"
|
||||||
alt={group.name}
|
|
||||||
title={group.name}
|
|
||||||
bg="muted"
|
bg="muted"
|
||||||
sx={{ borderRadius: "8px", gridGap: 0 }}
|
sx={{ borderRadius: "8px" }}
|
||||||
p={0}
|
p={0}
|
||||||
|
gap={2}
|
||||||
>
|
>
|
||||||
<Flex
|
<Flex
|
||||||
sx={{
|
sx={{
|
||||||
@ -57,7 +56,6 @@ function TokenBarTokenGroup({ group, tokens, draggable }) {
|
|||||||
}}
|
}}
|
||||||
onClick={(e) => handleOpenClick(e, false)}
|
onClick={(e) => handleOpenClick(e, false)}
|
||||||
key="group"
|
key="group"
|
||||||
alt={group.name}
|
|
||||||
title={group.name}
|
title={group.name}
|
||||||
{...listeners}
|
{...listeners}
|
||||||
{...attributes}
|
{...attributes}
|
||||||
@ -71,8 +69,6 @@ function TokenBarTokenGroup({ group, tokens, draggable }) {
|
|||||||
return (
|
return (
|
||||||
<Grid
|
<Grid
|
||||||
columns="1fr 1fr"
|
columns="1fr 1fr"
|
||||||
alt={group.name}
|
|
||||||
title={group.name}
|
|
||||||
bg="muted"
|
bg="muted"
|
||||||
sx={{
|
sx={{
|
||||||
borderRadius: "8px",
|
borderRadius: "8px",
|
||||||
@ -81,6 +77,8 @@ function TokenBarTokenGroup({ group, tokens, draggable }) {
|
|||||||
gridTemplateRows: "1fr 1fr",
|
gridTemplateRows: "1fr 1fr",
|
||||||
}}
|
}}
|
||||||
p="2px"
|
p="2px"
|
||||||
|
alt={group.name}
|
||||||
|
title={group.name}
|
||||||
{...listeners}
|
{...listeners}
|
||||||
{...attributes}
|
{...attributes}
|
||||||
>
|
>
|
||||||
|
@ -48,7 +48,14 @@ function TokenEditBar({ disabled, onLoad }) {
|
|||||||
async function handleTokensHide(hideInSidebar) {
|
async function handleTokensHide(hideInSidebar) {
|
||||||
const selectedTokens = getSelectedTokens();
|
const selectedTokens = getSelectedTokens();
|
||||||
const selectedTokenIds = selectedTokens.map((token) => token.id);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -4,6 +4,7 @@ import { Rect, Text, Group } from "react-konva";
|
|||||||
import useSetting from "../../hooks/useSetting";
|
import useSetting from "../../hooks/useSetting";
|
||||||
|
|
||||||
const maxTokenSize = 3;
|
const maxTokenSize = 3;
|
||||||
|
const defaultFontSize = 16;
|
||||||
|
|
||||||
function TokenLabel({ tokenState, width, height }) {
|
function TokenLabel({ tokenState, width, height }) {
|
||||||
const [labelSize] = useSetting("map.labelSize");
|
const [labelSize] = useSetting("map.labelSize");
|
||||||
@ -13,7 +14,7 @@ function TokenLabel({ tokenState, width, height }) {
|
|||||||
const paddingX =
|
const paddingX =
|
||||||
(height / 8 / tokenState.size) * Math.min(tokenState.size, maxTokenSize);
|
(height / 8 / tokenState.size) * Math.min(tokenState.size, maxTokenSize);
|
||||||
|
|
||||||
const [fontSize, setFontSize] = useState(1);
|
const [fontScale, setFontScale] = useState(0);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const text = textSizerRef.current;
|
const text = textSizerRef.current;
|
||||||
|
|
||||||
@ -22,15 +23,14 @@ function TokenLabel({ tokenState, width, height }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let fontSizes = [];
|
let fontSizes = [];
|
||||||
for (let size = 10 * labelSize; size >= 6; size--) {
|
for (let size = 20 * labelSize; size >= 6; size--) {
|
||||||
fontSizes.push(
|
const verticalSize = height / size / tokenState.size;
|
||||||
(height / size / tokenState.size) *
|
const tokenSize = Math.min(tokenState.size, maxTokenSize);
|
||||||
Math.min(tokenState.size, maxTokenSize) *
|
const fontSize = verticalSize * tokenSize * labelSize;
|
||||||
labelSize
|
fontSizes.push(fontSize);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function findFontSize() {
|
function findFontScale() {
|
||||||
const size = fontSizes.reduce((prev, curr) => {
|
const size = fontSizes.reduce((prev, curr) => {
|
||||||
text.fontSize(curr);
|
text.fontSize(curr);
|
||||||
const textWidth = text.getTextWidth() + paddingX * 2;
|
const textWidth = text.getTextWidth() + paddingX * 2;
|
||||||
@ -39,12 +39,12 @@ function TokenLabel({ tokenState, width, height }) {
|
|||||||
} else {
|
} else {
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
});
|
}, 1);
|
||||||
|
|
||||||
setFontSize(size);
|
setFontScale(size / defaultFontSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
findFontSize();
|
findFontScale();
|
||||||
}, [
|
}, [
|
||||||
tokenState.label,
|
tokenState.label,
|
||||||
tokenState.visible,
|
tokenState.visible,
|
||||||
@ -56,44 +56,47 @@ function TokenLabel({ tokenState, width, height }) {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const [rectWidth, setRectWidth] = useState(0);
|
const [rectWidth, setRectWidth] = useState(0);
|
||||||
|
const [textWidth, setTextWidth] = useState(0);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const text = textRef.current;
|
const text = textRef.current;
|
||||||
if (text && tokenState.label) {
|
if (text && tokenState.label) {
|
||||||
setRectWidth(text.getTextWidth() + paddingX * 2);
|
setRectWidth(text.getTextWidth() * fontScale + paddingX * 2);
|
||||||
|
setTextWidth(text.getTextWidth() * fontScale);
|
||||||
} else {
|
} else {
|
||||||
setRectWidth(0);
|
setRectWidth(0);
|
||||||
|
setTextWidth(0);
|
||||||
}
|
}
|
||||||
}, [tokenState.label, paddingX, width, fontSize]);
|
}, [tokenState.label, paddingX, width, fontScale]);
|
||||||
|
|
||||||
const textRef = useRef();
|
const textRef = useRef();
|
||||||
const textSizerRef = useRef();
|
const textSizerRef = useRef();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group y={height - (fontSize + paddingY) / 2}>
|
<Group y={height - (defaultFontSize * fontScale + paddingY) / 2}>
|
||||||
<Rect
|
<Rect
|
||||||
y={-paddingY / 2}
|
y={-paddingY / 2}
|
||||||
width={rectWidth}
|
width={rectWidth}
|
||||||
offsetX={width / 2}
|
offsetX={width / 2}
|
||||||
x={width - rectWidth / 2}
|
x={width - rectWidth / 2}
|
||||||
height={fontSize + paddingY}
|
height={defaultFontSize * fontScale + paddingY}
|
||||||
fill="hsla(230, 25%, 18%, 0.8)"
|
fill="hsla(230, 25%, 18%, 0.8)"
|
||||||
cornerRadius={(fontSize + paddingY) / 2}
|
cornerRadius={(defaultFontSize * fontScale + paddingY) / 2}
|
||||||
/>
|
|
||||||
<Text
|
|
||||||
ref={textRef}
|
|
||||||
width={width}
|
|
||||||
text={tokenState.label}
|
|
||||||
fontSize={fontSize}
|
|
||||||
lineHeight={1}
|
|
||||||
align="center"
|
|
||||||
verticalAlign="bottom"
|
|
||||||
fill="white"
|
|
||||||
paddingX={paddingX}
|
|
||||||
paddingY={paddingY}
|
|
||||||
wrap="none"
|
|
||||||
ellipsis={false}
|
|
||||||
hitFunc={() => {}}
|
|
||||||
/>
|
/>
|
||||||
|
<Group offsetX={(textWidth - width) / 2}>
|
||||||
|
<Text
|
||||||
|
ref={textRef}
|
||||||
|
text={tokenState.label}
|
||||||
|
fontSize={defaultFontSize}
|
||||||
|
lineHeight={1}
|
||||||
|
// Scale font instead of changing font size to avoid kerning issues with Firefox
|
||||||
|
scaleX={fontScale}
|
||||||
|
scaleY={fontScale}
|
||||||
|
fill="white"
|
||||||
|
wrap="none"
|
||||||
|
ellipsis={false}
|
||||||
|
hitFunc={() => {}}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
{/* Use an invisible text block to work out text sizing */}
|
{/* Use an invisible text block to work out text sizing */}
|
||||||
<Text
|
<Text
|
||||||
visible={false}
|
visible={false}
|
||||||
|
@ -50,7 +50,7 @@ function TokenMenu({
|
|||||||
}, [isOpen, tokenState, wasOpen, tokenImage]);
|
}, [isOpen, tokenState, wasOpen, tokenImage]);
|
||||||
|
|
||||||
function handleLabelChange(event) {
|
function handleLabelChange(event) {
|
||||||
const label = event.target.value.substring(0, 144);
|
const label = event.target.value.substring(0, 48);
|
||||||
tokenState && onTokenStateChange({ [tokenState.id]: { label: label } });
|
tokenState && onTokenStateChange({ [tokenState.id]: { label: label } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,8 +25,8 @@ function TokenTileGroup({
|
|||||||
<Grid
|
<Grid
|
||||||
columns={`repeat(${layout.groupGridColumns}, 1fr)`}
|
columns={`repeat(${layout.groupGridColumns}, 1fr)`}
|
||||||
p={2}
|
p={2}
|
||||||
|
gap={2}
|
||||||
sx={{
|
sx={{
|
||||||
gridGap: 2,
|
|
||||||
height: "100%",
|
height: "100%",
|
||||||
gridTemplateRows: `repeat(${layout.groupGridColumns}, 1fr)`,
|
gridTemplateRows: `repeat(${layout.groupGridColumns}, 1fr)`,
|
||||||
}}
|
}}
|
||||||
|
@ -5,6 +5,7 @@ import TokenTileGroup from "./TokenTileGroup";
|
|||||||
import TokenHiddenBadge from "./TokenHiddenBadge";
|
import TokenHiddenBadge from "./TokenHiddenBadge";
|
||||||
|
|
||||||
import SortableTiles from "../tile/SortableTiles";
|
import SortableTiles from "../tile/SortableTiles";
|
||||||
|
import SortableTilesDragOverlay from "../tile/SortableTilesDragOverlay";
|
||||||
|
|
||||||
import { getGroupItems } from "../../helpers/group";
|
import { getGroupItems } from "../../helpers/group";
|
||||||
|
|
||||||
@ -21,24 +22,28 @@ function TokenTiles({ tokensById, onTokenEdit, subgroup }) {
|
|||||||
function renderTile(group) {
|
function renderTile(group) {
|
||||||
if (group.type === "item") {
|
if (group.type === "item") {
|
||||||
const token = tokensById[group.id];
|
const token = tokensById[group.id];
|
||||||
const isSelected = selectedGroupIds.includes(group.id);
|
if (token) {
|
||||||
const canEdit =
|
const isSelected = selectedGroupIds.includes(group.id);
|
||||||
isSelected && selectMode === "single" && selectedGroupIds.length === 1;
|
const canEdit =
|
||||||
|
isSelected &&
|
||||||
|
selectMode === "single" &&
|
||||||
|
selectedGroupIds.length === 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TokenTile
|
<TokenTile
|
||||||
key={token.id}
|
key={token.id}
|
||||||
token={token}
|
token={token}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
onSelect={onGroupSelect}
|
onSelect={onGroupSelect}
|
||||||
onTokenEdit={onTokenEdit}
|
onTokenEdit={onTokenEdit}
|
||||||
canEdit={canEdit}
|
canEdit={canEdit}
|
||||||
badges={[
|
badges={[
|
||||||
`${token.defaultSize}x`,
|
`${token.defaultSize}x`,
|
||||||
<TokenHiddenBadge hidden={token.hideInSidebar} />,
|
<TokenHiddenBadge hidden={token.hideInSidebar} />,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const isSelected = selectedGroupIds.includes(group.id);
|
const isSelected = selectedGroupIds.includes(group.id);
|
||||||
const items = getGroupItems(group);
|
const items = getGroupItems(group);
|
||||||
@ -57,7 +62,12 @@ function TokenTiles({ tokensById, onTokenEdit, subgroup }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return <SortableTiles renderTile={renderTile} subgroup={subgroup} />;
|
return (
|
||||||
|
<>
|
||||||
|
<SortableTiles renderTile={renderTile} subgroup={subgroup} />
|
||||||
|
<SortableTilesDragOverlay renderTile={renderTile} subgroup={subgroup} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TokenTiles;
|
export default TokenTiles;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useContext } from "react";
|
import React, { useState, useContext, useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
MouseSensor,
|
MouseSensor,
|
||||||
TouchSensor,
|
TouchSensor,
|
||||||
@ -16,7 +16,9 @@ import { moveGroupsInto, moveGroups, ungroup } from "../helpers/group";
|
|||||||
|
|
||||||
import usePreventSelect from "../hooks/usePreventSelect";
|
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 BASE_SORTABLE_ID = "__base__";
|
||||||
export const GROUP_SORTABLE_ID = "__group__";
|
export const GROUP_SORTABLE_ID = "__group__";
|
||||||
@ -61,7 +63,7 @@ export function TileDragProvider({
|
|||||||
} = useGroup();
|
} = useGroup();
|
||||||
|
|
||||||
const mouseSensor = useSensor(MouseSensor, {
|
const mouseSensor = useSensor(MouseSensor, {
|
||||||
activationConstraint: { delay: 250, tolerance: 5 },
|
activationConstraint: { distance: 3 },
|
||||||
});
|
});
|
||||||
const touchSensor = useSensor(TouchSensor, {
|
const touchSensor = useSensor(TouchSensor, {
|
||||||
activationConstraint: { delay: 250, tolerance: 5 },
|
activationConstraint: { delay: 250, tolerance: 5 },
|
||||||
@ -70,16 +72,23 @@ export function TileDragProvider({
|
|||||||
|
|
||||||
const sensors = useSensors(mouseSensor, touchSensor, keyboardSensor);
|
const sensors = useSensors(mouseSensor, touchSensor, keyboardSensor);
|
||||||
|
|
||||||
const [dragId, setDragId] = useState();
|
const [dragId, setDragId] = useState(null);
|
||||||
const [overId, setOverId] = useState();
|
const [overId, setOverId] = useState(null);
|
||||||
const [dragCursor, setDragCursor] = useState("pointer");
|
const [dragCursor, setDragCursor] = useState("pointer");
|
||||||
|
|
||||||
const [preventSelect, resumeSelect] = usePreventSelect();
|
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) {
|
function handleDragStart(event) {
|
||||||
const { active, over } = event;
|
const { active, over } = event;
|
||||||
setDragId(active.id);
|
setDragId(active.id);
|
||||||
setOverId(over?.id);
|
setOverId(over?.id || null);
|
||||||
if (!selectedGroupIds.includes(active.id)) {
|
if (!selectedGroupIds.includes(active.id)) {
|
||||||
onGroupSelect(active.id);
|
onGroupSelect(active.id);
|
||||||
}
|
}
|
||||||
@ -93,7 +102,7 @@ export function TileDragProvider({
|
|||||||
function handleDragOver(event) {
|
function handleDragOver(event) {
|
||||||
const { over } = event;
|
const { over } = event;
|
||||||
|
|
||||||
setOverId(over?.id);
|
setOverId(over?.id || null);
|
||||||
if (over) {
|
if (over) {
|
||||||
if (
|
if (
|
||||||
over.id.startsWith(UNGROUP_ID) ||
|
over.id.startsWith(UNGROUP_ID) ||
|
||||||
@ -111,8 +120,8 @@ export function TileDragProvider({
|
|||||||
function handleDragEnd(event) {
|
function handleDragEnd(event) {
|
||||||
const { active, over, overlayNodeClientRect } = event;
|
const { active, over, overlayNodeClientRect } = event;
|
||||||
|
|
||||||
setDragId();
|
setDragId(null);
|
||||||
setOverId();
|
setOverId(null);
|
||||||
setDragCursor("pointer");
|
setDragCursor("pointer");
|
||||||
if (active && over && active.id !== over.id) {
|
if (active && over && active.id !== over.id) {
|
||||||
let selectedIndices = selectedGroupIds.map((groupId) =>
|
let selectedIndices = selectedGroupIds.map((groupId) =>
|
||||||
@ -165,8 +174,8 @@ export function TileDragProvider({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleDragCancel(event) {
|
function handleDragCancel(event) {
|
||||||
setDragId();
|
setDragId(null);
|
||||||
setOverId();
|
setOverId(null);
|
||||||
setDragCursor("pointer");
|
setDragCursor("pointer");
|
||||||
|
|
||||||
resumeSelect();
|
resumeSelect();
|
||||||
@ -210,8 +219,6 @@ export function TileDragProvider({
|
|||||||
return closestCenter(otherRects, rect);
|
return closestCenter(otherRects, rect);
|
||||||
}
|
}
|
||||||
|
|
||||||
const value = { dragId, overId, dragCursor };
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DragContext
|
<DragContext
|
||||||
onDragStart={handleDragStart}
|
onDragStart={handleDragStart}
|
||||||
@ -221,19 +228,37 @@ export function TileDragProvider({
|
|||||||
sensors={sensors}
|
sensors={sensors}
|
||||||
collisionDetection={customCollisionDetection}
|
collisionDetection={customCollisionDetection}
|
||||||
>
|
>
|
||||||
<TileDragContext.Provider value={value}>
|
<TileDragIdContext.Provider value={dragId}>
|
||||||
{children}
|
<TileOverGroupIdContext.Provider value={overGroupId}>
|
||||||
</TileDragContext.Provider>
|
<TileDragCursorContext.Provider value={dragCursor}>
|
||||||
|
{children}
|
||||||
|
</TileDragCursorContext.Provider>
|
||||||
|
</TileOverGroupIdContext.Provider>
|
||||||
|
</TileDragIdContext.Provider>
|
||||||
</DragContext>
|
</DragContext>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTileDrag() {
|
export function useTileDragId() {
|
||||||
const context = useContext(TileDragContext);
|
const context = useContext(TileDragIdContext);
|
||||||
if (context === undefined) {
|
if (context === undefined) {
|
||||||
throw new Error("useTileDrag must be used within a TileDragProvider");
|
throw new Error("useTileDrag must be used within a TileDragProvider");
|
||||||
}
|
}
|
||||||
return context;
|
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;
|
||||||
|
}
|
||||||
|
@ -115,30 +115,24 @@ export function TokenDataProvider({ children }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleTokenChanges(changes) {
|
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) {
|
for (let change of changes) {
|
||||||
if (change.table === "tokens") {
|
if (change.table === "tokens") {
|
||||||
if (change.type === 1) {
|
if (change.type === 1) {
|
||||||
// Created
|
// Created
|
||||||
const token = change.obj;
|
const token = change.obj;
|
||||||
setTokens((prevTokens) => [token, ...prevTokens]);
|
tokensCreated.push(token);
|
||||||
} else if (change.type === 2) {
|
} else if (change.type === 2) {
|
||||||
// Updated
|
// Updated
|
||||||
const token = change.obj;
|
const token = change.obj;
|
||||||
setTokens((prevTokens) => {
|
tokensUpdated[token.id] = token;
|
||||||
const newTokens = [...prevTokens];
|
|
||||||
const i = newTokens.findIndex((t) => t.id === token.id);
|
|
||||||
if (i > -1) {
|
|
||||||
newTokens[i] = token;
|
|
||||||
}
|
|
||||||
return newTokens;
|
|
||||||
});
|
|
||||||
} else if (change.type === 3) {
|
} else if (change.type === 3) {
|
||||||
// Deleted
|
// Deleted
|
||||||
const id = change.key;
|
const id = change.key;
|
||||||
setTokens((prevTokens) => {
|
tokensDeleted.push(id);
|
||||||
const filtered = prevTokens.filter((token) => token.id !== id);
|
|
||||||
return filtered;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (change.table === "groups") {
|
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);
|
database.on("changes", handleTokenChanges);
|
||||||
|
@ -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.
|
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.
|
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.
|
- 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.
|
- 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.
|
- Modals have a new transition animation to match the new group UI.
|
||||||
- Cursors now better represent drag and drop actions.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
||||||
|
@ -289,10 +289,7 @@ export function gridDistance(grid, a, b, cellSize) {
|
|||||||
const bCoord = getNearestCellCoordinates(grid, b.x, b.y, cellSize);
|
const bCoord = getNearestCellCoordinates(grid, b.x, b.y, cellSize);
|
||||||
if (grid.type === "square") {
|
if (grid.type === "square") {
|
||||||
if (grid.measurement.type === "chebyshev") {
|
if (grid.measurement.type === "chebyshev") {
|
||||||
return Math.max(
|
return Vector2.max(Vector2.abs(Vector2.subtract(aCoord, bCoord)));
|
||||||
Math.abs(aCoord.x - bCoord.x),
|
|
||||||
Math.abs(aCoord.y - bCoord.y)
|
|
||||||
);
|
|
||||||
} else if (grid.measurement.type === "alternating") {
|
} else if (grid.measurement.type === "alternating") {
|
||||||
// Alternating diagonal distance like D&D 3.5 and Pathfinder
|
// Alternating diagonal distance like D&D 3.5 and Pathfinder
|
||||||
const delta = Vector2.abs(Vector2.subtract(aCoord, bCoord));
|
const delta = Vector2.abs(Vector2.subtract(aCoord, bCoord));
|
||||||
@ -300,7 +297,7 @@ export function gridDistance(grid, a, b, cellSize) {
|
|||||||
const min = Vector2.min(delta);
|
const min = Vector2.min(delta);
|
||||||
return max - min + Math.floor(1.5 * min);
|
return max - min + Math.floor(1.5 * min);
|
||||||
} else if (grid.measurement.type === "euclidean") {
|
} 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") {
|
} else if (grid.measurement.type === "manhattan") {
|
||||||
return Math.abs(aCoord.x - bCoord.x) + Math.abs(aCoord.y - bCoord.y);
|
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
|
2
|
||||||
);
|
);
|
||||||
} else if (grid.measurement.type === "euclidean") {
|
} else if (grid.measurement.type === "euclidean") {
|
||||||
return Vector2.distance(aCoord, bCoord);
|
return Vector2.length(Vector2.divide(Vector2.subtract(a, b), cellSize));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -189,41 +189,64 @@ export async function createThumbnail(image, type, size = 300, quality = 0.5) {
|
|||||||
* @returns {Outline}
|
* @returns {Outline}
|
||||||
*/
|
*/
|
||||||
export function getImageOutline(image, maxPoints = 100) {
|
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 (outlinePoints) {
|
||||||
if (baseOutline.length > maxPoints) {
|
if (outlinePoints.length > maxPoints) {
|
||||||
baseOutline = Vector2.resample(baseOutline, maxPoints);
|
outlinePoints = Vector2.resample(outlinePoints, maxPoints);
|
||||||
}
|
}
|
||||||
const bounds = Vector2.getBoundingBox(baseOutline);
|
const bounds = Vector2.getBoundingBox(outlinePoints);
|
||||||
if (Vector2.rectangular(baseOutline)) {
|
|
||||||
return {
|
// Reject outline if it's area is less than 5% of the image
|
||||||
type: "rect",
|
const imageArea = image.width * image.height;
|
||||||
x: Math.round(bounds.min.x),
|
const area = bounds.width * bounds.height;
|
||||||
y: Math.round(bounds.min.y),
|
if (area < imageArea * 0.05) {
|
||||||
width: Math.round(bounds.width),
|
return defaultOutline;
|
||||||
height: Math.round(bounds.height),
|
}
|
||||||
};
|
|
||||||
} else if (
|
// Detect if the outline is a rectangle or circle
|
||||||
Vector2.circular(
|
if (Vector2.rectangular(outlinePoints)) {
|
||||||
baseOutline,
|
return {
|
||||||
Math.max(bounds.width / 10, bounds.height / 10)
|
type: "rect",
|
||||||
)
|
x: Math.round(bounds.min.x),
|
||||||
) {
|
y: Math.round(bounds.min.y),
|
||||||
return {
|
width: Math.round(bounds.width),
|
||||||
type: "circle",
|
height: Math.round(bounds.height),
|
||||||
x: Math.round(bounds.center.x),
|
};
|
||||||
y: Math.round(bounds.center.y),
|
} else if (
|
||||||
radius: Math.round(Math.min(bounds.width, bounds.height) / 2),
|
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 {
|
} else {
|
||||||
// Flatten and round outline to save on storage size
|
return defaultOutline;
|
||||||
const points = baseOutline
|
|
||||||
.map(({ x, y }) => [Math.round(x), Math.round(y)])
|
|
||||||
.flat();
|
|
||||||
return { type: "path", points };
|
|
||||||
}
|
}
|
||||||
} else {
|
} catch {
|
||||||
return { type: "rect", x: 0, y: 0, width: 1, height: 1 };
|
return defaultOutline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,24 @@ export async function createTokenFromFile(file, userId) {
|
|||||||
return Promise.reject();
|
return Promise.reject();
|
||||||
}
|
}
|
||||||
let name = "Unknown Token";
|
let name = "Unknown Token";
|
||||||
|
let defaultSize = 1;
|
||||||
if (file.name) {
|
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
|
// Remove file extension
|
||||||
name = file.name.replace(/\.[^/.]+$/, "");
|
name = file.name.replace(/\.[^/.]+$/, "");
|
||||||
// Removed grid size expression
|
// Removed grid size expression
|
||||||
@ -79,6 +96,7 @@ export async function createTokenFromFile(file, userId) {
|
|||||||
|
|
||||||
const token = {
|
const token = {
|
||||||
name,
|
name,
|
||||||
|
defaultSize,
|
||||||
thumbnail: thumbnail.id,
|
thumbnail: thumbnail.id,
|
||||||
file: fileAsset.id,
|
file: fileAsset.id,
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
@ -86,7 +104,6 @@ export async function createTokenFromFile(file, userId) {
|
|||||||
created: Date.now(),
|
created: Date.now(),
|
||||||
lastModified: Date.now(),
|
lastModified: Date.now(),
|
||||||
owner: userId,
|
owner: userId,
|
||||||
defaultSize: 1,
|
|
||||||
defaultCategory: "character",
|
defaultCategory: "character",
|
||||||
defaultLabel: "",
|
defaultLabel: "",
|
||||||
hideInSidebar: false,
|
hideInSidebar: false,
|
||||||
|
@ -17,12 +17,16 @@ import {
|
|||||||
/**
|
/**
|
||||||
* Returns a function that when called will snap a node to the current grid
|
* 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 {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(
|
const [defaultSnappingSensitivity] = useSetting(
|
||||||
"map.gridSnappingSensitivity"
|
"map.gridSnappingSensitivity"
|
||||||
);
|
);
|
||||||
snappingSensitivity = snappingSensitivity || defaultSnappingSensitivity;
|
snappingSensitivity =
|
||||||
|
snappingSensitivity === undefined
|
||||||
|
? defaultSnappingSensitivity
|
||||||
|
: snappingSensitivity;
|
||||||
|
|
||||||
const grid = useGrid();
|
const grid = useGrid();
|
||||||
const gridOffset = useGridOffset();
|
const gridOffset = useGridOffset();
|
||||||
@ -57,7 +61,10 @@ function useGridSnapping(snappingSensitivity) {
|
|||||||
gridCellPixelSize
|
gridCellPixelSize
|
||||||
);
|
);
|
||||||
|
|
||||||
const snapPoints = [cellPosition, ...cellCorners];
|
const snapPoints = [cellPosition];
|
||||||
|
if (useCorners) {
|
||||||
|
snapPoints.push(...cellCorners);
|
||||||
|
}
|
||||||
|
|
||||||
for (let snapPoint of snapPoints) {
|
for (let snapPoint of snapPoints) {
|
||||||
const distanceToSnapPoint = Vector2.distance(offsetPosition, snapPoint);
|
const distanceToSnapPoint = Vector2.distance(offsetPosition, snapPoint);
|
||||||
|
@ -1,5 +1,14 @@
|
|||||||
function usePreventSelect() {
|
function usePreventSelect() {
|
||||||
|
function clearSelection() {
|
||||||
|
if (window.getSelection) {
|
||||||
|
window.getSelection().removeAllRanges();
|
||||||
|
}
|
||||||
|
if (document.selection) {
|
||||||
|
document.selection.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
function preventSelect() {
|
function preventSelect() {
|
||||||
|
clearSelection();
|
||||||
document.body.classList.add("no-select");
|
document.body.classList.add("no-select");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import * as Sentry from "@sentry/react";
|
import * as Sentry from "@sentry/react";
|
||||||
|
import { Dedupe } from "@sentry/integrations";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
import Modal from "react-modal";
|
import Modal from "react-modal";
|
||||||
|
|
||||||
@ -16,10 +17,16 @@ if (!("PointerEvent" in window)) {
|
|||||||
import("pepjs");
|
import("pepjs");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Intersection observer polyfill
|
||||||
|
if (!("IntersectionObserver" in window)) {
|
||||||
|
import("intersection-observer");
|
||||||
|
}
|
||||||
|
|
||||||
if (process.env.REACT_APP_LOGGING === "true") {
|
if (process.env.REACT_APP_LOGGING === "true") {
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn: process.env.REACT_APP_SENTRY_DSN,
|
dsn: process.env.REACT_APP_SENTRY_DSN,
|
||||||
release: "owlbear-rodeo@" + process.env.REACT_APP_VERSION,
|
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 resize error as it is triggered by going fullscreen on slower computers
|
||||||
// Ignore quota error
|
// Ignore quota error
|
||||||
// Ignore XDR encoding failure bug in Firefox https://bugzilla.mozilla.org/show_bug.cgi?id=1678243
|
// Ignore XDR encoding failure bug in Firefox https://bugzilla.mozilla.org/show_bug.cgi?id=1678243
|
||||||
|
Before Width: | Height: | Size: 65 KiB |
BIN
src/maps/Blank.jpg
Executable file
After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 67 KiB |
BIN
src/maps/Grass.jpg
Executable file
After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 71 KiB |
BIN
src/maps/Sand.jpg
Executable file
After Width: | Height: | Size: 86 KiB |
Before Width: | Height: | Size: 91 KiB |
BIN
src/maps/Stone.jpg
Executable file
After Width: | Height: | Size: 180 KiB |
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
Before Width: | Height: | Size: 105 KiB |
BIN
src/maps/Water.jpg
Executable file
After Width: | Height: | Size: 132 KiB |
Before Width: | Height: | Size: 109 KiB |
BIN
src/maps/Wood.jpg
Executable file
After Width: | Height: | Size: 262 KiB |
@ -1,13 +1,13 @@
|
|||||||
import Case from "case";
|
import Case from "case";
|
||||||
|
|
||||||
import blankImage from "./Blank Grid 22x22.jpg";
|
import blankImage from "./Blank.jpg";
|
||||||
import grassImage from "./Grass Grid 22x22.jpg";
|
import grassImage from "./Grass.jpg";
|
||||||
import sandImage from "./Sand Grid 22x22.jpg";
|
import sandImage from "./Sand.jpg";
|
||||||
import stoneImage from "./Stone Grid 22x22.jpg";
|
import stoneImage from "./Stone.jpg";
|
||||||
import waterImage from "./Water Grid 22x22.jpg";
|
import waterImage from "./Water.jpg";
|
||||||
import woodImage from "./Wood Grid 22x22.jpg";
|
import woodImage from "./Wood.jpg";
|
||||||
|
|
||||||
import unknownImage from "./Unknown Grid 22x22.jpg";
|
import unknownImage from "./Unknown.jpg";
|
||||||
|
|
||||||
export const mapSources = {
|
export const mapSources = {
|
||||||
blank: blankImage,
|
blank: blankImage,
|
||||||
@ -42,7 +42,7 @@ export function getDefaultMaps(userId) {
|
|||||||
type: "default",
|
type: "default",
|
||||||
created: mapKeys.length - i,
|
created: mapKeys.length - i,
|
||||||
lastModified: Date.now(),
|
lastModified: Date.now(),
|
||||||
showGrid: false,
|
showGrid: key !== "stone",
|
||||||
snapToGrid: true,
|
snapToGrid: true,
|
||||||
group: "",
|
group: "",
|
||||||
};
|
};
|
||||||
|
@ -132,7 +132,7 @@ function EditMapModal({
|
|||||||
<SimpleBar
|
<SimpleBar
|
||||||
style={{
|
style={{
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
padding: "0 20px",
|
padding: "16px",
|
||||||
backgroundColor: theme.colors.muted,
|
backgroundColor: theme.colors.muted,
|
||||||
margin: "0 8px",
|
margin: "0 8px",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
|
@ -77,7 +77,7 @@ function EditTokenModal({ isOpen, onDone, token, onUpdateToken }) {
|
|||||||
<SimpleBar
|
<SimpleBar
|
||||||
style={{
|
style={{
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
padding: "0 20px",
|
padding: "16px",
|
||||||
backgroundColor: theme.colors.muted,
|
backgroundColor: theme.colors.muted,
|
||||||
margin: "0 8px",
|
margin: "0 8px",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
|
@ -237,65 +237,79 @@ function SelectDataModal({
|
|||||||
function renderMapGroup(group) {
|
function renderMapGroup(group) {
|
||||||
if (group.type === "item") {
|
if (group.type === "item") {
|
||||||
const map = maps[group.id];
|
const map = maps[group.id];
|
||||||
return (
|
if (map) {
|
||||||
<Label key={map.id} my={1} pl={4} sx={{ fontFamily: "body2" }}>
|
return (
|
||||||
<Checkbox
|
<Label key={map.id} my={1} pl={4} sx={{ fontFamily: "body2" }}>
|
||||||
checked={map.checked}
|
<Checkbox
|
||||||
onChange={(e) => handleMapsChanged(e, [map])}
|
checked={map.checked}
|
||||||
/>
|
onChange={(e) => handleMapsChanged(e, [map])}
|
||||||
{map.name}
|
/>
|
||||||
</Label>
|
{map.name}
|
||||||
);
|
</Label>
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return renderGroupContainer(
|
if (group.items.some((item) => item.id in maps)) {
|
||||||
group,
|
return renderGroupContainer(
|
||||||
group.items.some((item) => maps[item.id].checked),
|
group,
|
||||||
renderMapGroup,
|
group.items.some((item) => maps[item.id]?.checked),
|
||||||
(e, group) =>
|
renderMapGroup,
|
||||||
handleMapsChanged(
|
(e, group) =>
|
||||||
e,
|
handleMapsChanged(
|
||||||
group.items.map((group) => maps[group.id])
|
e,
|
||||||
)
|
group.items
|
||||||
);
|
.filter((group) => group.id in maps)
|
||||||
|
.map((group) => maps[group.id])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTokenGroup(group) {
|
function renderTokenGroup(group) {
|
||||||
if (group.type === "item") {
|
if (group.type === "item") {
|
||||||
const token = tokens[group.id];
|
const token = tokens[group.id];
|
||||||
return (
|
if (token) {
|
||||||
<Box pl={4} my={1} key={token.id}>
|
return (
|
||||||
<Label sx={{ fontFamily: "body2" }}>
|
<Box pl={4} my={1} key={token.id}>
|
||||||
<Checkbox
|
<Label sx={{ fontFamily: "body2" }}>
|
||||||
checked={token.checked}
|
<Checkbox
|
||||||
onChange={(e) => handleTokensChanged(e, [token])}
|
checked={token.checked}
|
||||||
disabled={token.type !== "default" && token.id in tokenUsedCount}
|
onChange={(e) => handleTokensChanged(e, [token])}
|
||||||
/>
|
disabled={
|
||||||
{token.name}
|
token.type !== "default" && token.id in tokenUsedCount
|
||||||
</Label>
|
}
|
||||||
{token.id in tokenUsedCount && token.type !== "default" && (
|
/>
|
||||||
<Text as="p" variant="caption" ml={4}>
|
{token.name}
|
||||||
Token used in {tokenUsedCount[token.id]} selected map
|
</Label>
|
||||||
{tokenUsedCount[token.id] > 1 && "s"}
|
{token.id in tokenUsedCount && token.type !== "default" && (
|
||||||
</Text>
|
<Text as="p" variant="caption" ml={4}>
|
||||||
)}
|
Token used in {tokenUsedCount[token.id]} selected map
|
||||||
</Box>
|
{tokenUsedCount[token.id] > 1 && "s"}
|
||||||
);
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const checked =
|
if (group.items.some((item) => item.id in tokens)) {
|
||||||
group.items.some(
|
const checked =
|
||||||
(item) => !(item.id in tokenUsedCount) && tokens[item.id].checked
|
group.items.some(
|
||||||
) || group.items.every((item) => item.id in tokenUsedCount);
|
(item) => !(item.id in tokenUsedCount) && tokens[item.id]?.checked
|
||||||
return renderGroupContainer(
|
) || group.items.every((item) => item.id in tokenUsedCount);
|
||||||
group,
|
return renderGroupContainer(
|
||||||
checked,
|
group,
|
||||||
renderTokenGroup,
|
checked,
|
||||||
(e, group) =>
|
renderTokenGroup,
|
||||||
handleTokensChanged(
|
(e, group) =>
|
||||||
e,
|
handleTokensChanged(
|
||||||
group.items.map((group) => tokens[group.id])
|
e,
|
||||||
)
|
group.items
|
||||||
);
|
.filter((group) => group.id in tokens)
|
||||||
|
.map((group) => tokens[group.id])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
10
src/theme.js
@ -16,7 +16,7 @@ const theme = {
|
|||||||
background: "hsl(10, 10%, 98%)",
|
background: "hsl(10, 10%, 98%)",
|
||||||
primary: "hsl(260, 100%, 80%)",
|
primary: "hsl(260, 100%, 80%)",
|
||||||
secondary: "hsl(290, 100%, 80%)",
|
secondary: "hsl(290, 100%, 80%)",
|
||||||
highlight: "hsl(260, 20%, 40%)",
|
highlight: "hsl(260, 20%, 70%)",
|
||||||
muted: "hsla(230, 20%, 60%, 20%)",
|
muted: "hsla(230, 20%, 60%, 20%)",
|
||||||
overlay: "hsla(230, 100%, 97%, 80%)",
|
overlay: "hsla(230, 100%, 97%, 80%)",
|
||||||
border: "hsla(10, 20%, 20%, 0.5)",
|
border: "hsla(10, 20%, 20%, 0.5)",
|
||||||
@ -188,6 +188,14 @@ const theme = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
textarea: {
|
textarea: {
|
||||||
|
"&:focus": {
|
||||||
|
outlineColor: "primary",
|
||||||
|
},
|
||||||
|
"&:disabled": {
|
||||||
|
backgroundColor: "muted",
|
||||||
|
opacity: 0.5,
|
||||||
|
borderColor: "text",
|
||||||
|
},
|
||||||
fontFamily: "body2",
|
fontFamily: "body2",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
160
yarn.lock
@ -2312,68 +2312,78 @@
|
|||||||
estree-walker "^1.0.1"
|
estree-walker "^1.0.1"
|
||||||
picomatch "^2.2.2"
|
picomatch "^2.2.2"
|
||||||
|
|
||||||
"@sentry/browser@6.2.2":
|
"@sentry/browser@6.3.0":
|
||||||
version "6.2.2"
|
version "6.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-6.2.2.tgz#4df4ad7026b269d85b63b79a75387ce5370bc705"
|
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-6.3.0.tgz#523430667bff290220909fb96f35706ff808fed6"
|
||||||
integrity sha512-K5UGyEePtVPZIFMoiRafhd4Ov0M1kdozVsVKIPZrOpJyjQdPNX+fYDNL/h0nVmgOlE2S/uu4fl4mEfe/6aLShw==
|
integrity sha512-Rse9j5XwN9n7GnfW1mNscTS4YQ0oiBNJcaSk3Mw/vQT872Wh60yKyx5wxAw5GujFZI0NgdyPlZwZ/tGQwirRxA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@sentry/core" "6.2.2"
|
"@sentry/core" "6.3.0"
|
||||||
"@sentry/types" "6.2.2"
|
"@sentry/types" "6.3.0"
|
||||||
"@sentry/utils" "6.2.2"
|
"@sentry/utils" "6.3.0"
|
||||||
tslib "^1.9.3"
|
tslib "^1.9.3"
|
||||||
|
|
||||||
"@sentry/core@6.2.2":
|
"@sentry/core@6.3.0":
|
||||||
version "6.2.2"
|
version "6.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-6.2.2.tgz#ec86b5769f8855f43cb58e839f81f87074ec9a3f"
|
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-6.3.0.tgz#3b8db24918a00c0b77f1663fc6d9be925f66bb3e"
|
||||||
integrity sha512-qqWbvvXtymfXh7N5eEvk97MCnMURuyFIgqWdVD4MQM6yIfDCy36CyGfuQ3ViHTLZGdIfEOhLL9/f4kzf1RzqBA==
|
integrity sha512-voot/lJ9gRXB6bx6tVqbEbD6jOd4Sx6Rfmm6pzfpom9C0q+fjIZTatTLq8GdXj8DzxaH1MBDSwtaq/eC3NqYpA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@sentry/hub" "6.2.2"
|
"@sentry/hub" "6.3.0"
|
||||||
"@sentry/minimal" "6.2.2"
|
"@sentry/minimal" "6.3.0"
|
||||||
"@sentry/types" "6.2.2"
|
"@sentry/types" "6.3.0"
|
||||||
"@sentry/utils" "6.2.2"
|
"@sentry/utils" "6.3.0"
|
||||||
tslib "^1.9.3"
|
tslib "^1.9.3"
|
||||||
|
|
||||||
"@sentry/hub@6.2.2":
|
"@sentry/hub@6.3.0":
|
||||||
version "6.2.2"
|
version "6.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-6.2.2.tgz#f451d8d3ad207e81556b4846d810226693e0444e"
|
resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-6.3.0.tgz#4225b3b0f31fe47f24d80753b257a4b57de5d651"
|
||||||
integrity sha512-VR6uQGRYt6RP633FHShlSLj0LUKGVrlTeSlwCoooWM5FR9lmi6akAaweuxpG78/kZvXrAWpjX6/nuYwHKGwzGA==
|
integrity sha512-lAnW3Om66t9IR+t1wya1NpOF9lGbvYG6Ca8wxJJGJ1t2PxKwyxpZKzRx0q8M1QFhlZ5cETCzxmM7lBEZ4QVCBg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@sentry/types" "6.2.2"
|
"@sentry/types" "6.3.0"
|
||||||
"@sentry/utils" "6.2.2"
|
"@sentry/utils" "6.3.0"
|
||||||
tslib "^1.9.3"
|
tslib "^1.9.3"
|
||||||
|
|
||||||
"@sentry/minimal@6.2.2":
|
"@sentry/integrations@^6.3.0":
|
||||||
version "6.2.2"
|
version "6.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-6.2.2.tgz#01f41e0a6a6a2becfc99f6bb6f9c4bddf54f8dae"
|
resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-6.3.0.tgz#2091889d1d4a319d48e84ebce43405c6a7fee5b7"
|
||||||
integrity sha512-l0IgoGQgg1lTd4qDU8bQn25sbZBg8PwIHfuTLbGMlRr1flDXHOM1UXajWK/UKbAPelnU7M2JBSVzgl7PwjprzA==
|
integrity sha512-/bl0wykJr+7zJHmnAulI+/J1kT5AI/019jWSXX7nmfIhp2sRXNUw0jeNVh+xfwrbR6Ik6IleAyzwHNYKzedGVQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@sentry/hub" "6.2.2"
|
"@sentry/types" "6.3.0"
|
||||||
"@sentry/types" "6.2.2"
|
"@sentry/utils" "6.3.0"
|
||||||
|
localforage "^1.8.1"
|
||||||
tslib "^1.9.3"
|
tslib "^1.9.3"
|
||||||
|
|
||||||
"@sentry/react@^6.2.2":
|
"@sentry/minimal@6.3.0":
|
||||||
version "6.2.2"
|
version "6.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/@sentry/react/-/react-6.2.2.tgz#6a93fa1013b2b9e37a8c0bc16cf0cbf4353de4c6"
|
resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-6.3.0.tgz#e64d87c92a4676a11168672a96589f46985f2b22"
|
||||||
integrity sha512-yDuxPOD4j2WE5nX1p48GIqXwrrmwkjryFjtYvLgzGJkiGWLmGTrxrSqtUKrbqahJpKt3mi24Nkg0cMlsFB178g==
|
integrity sha512-ZdPUwdPQkaKroy67NkwQRqmnfKyd/C1OyouM9IqYKyBjAInjOijwwc/Rd91PMHalvCOGfp1scNZYbZ+YFs/qQQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@sentry/browser" "6.2.2"
|
"@sentry/hub" "6.3.0"
|
||||||
"@sentry/minimal" "6.2.2"
|
"@sentry/types" "6.3.0"
|
||||||
"@sentry/types" "6.2.2"
|
tslib "^1.9.3"
|
||||||
"@sentry/utils" "6.2.2"
|
|
||||||
|
"@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"
|
hoist-non-react-statics "^3.3.2"
|
||||||
tslib "^1.9.3"
|
tslib "^1.9.3"
|
||||||
|
|
||||||
"@sentry/types@6.2.2":
|
"@sentry/types@6.3.0":
|
||||||
version "6.2.2"
|
version "6.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.2.2.tgz#9fc7795156680d3da5fc6ecc66702d8f7917f2b1"
|
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.3.0.tgz#919cc1870f34b7126546c77e3c695052795d3add"
|
||||||
integrity sha512-Y/1sRtw3a5JU4YdNBig8lLSVJ1UdYtuge+QP1CVLcLSAbq07Ok1bvF+Z+BlNcnHqle2Fl8aKuryG5Yu86enOyQ==
|
integrity sha512-xWyCYDmFPjS5ex60kxOOHbHEs4vs00qHbm0iShQfjl4OSg9S2azkcWofDmX8Xbn0FSOUXgdPCjNJW1B0bPVhCA==
|
||||||
|
|
||||||
"@sentry/utils@6.2.2":
|
"@sentry/utils@6.3.0":
|
||||||
version "6.2.2"
|
version "6.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.2.2.tgz#69f7151db74e65a010cec062cc9ab3e30bf2c80a"
|
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.3.0.tgz#e28645b6d4acd03a478e58bfe112ea02f81e94a0"
|
||||||
integrity sha512-qaee6X6VDNZ8HeO83/veaKw0KuhDE7j1R+Yryme3PywFzsoTzutDrEQjb7gvcHAhBaAYX8IHUBHgxcFI9BxI+w==
|
integrity sha512-NZzw4oLelgvCsVBG2e+ZtFtaBvgA7rZYtcGFbZTphhAlYoJ6JMCQUzYk0iwJK79yR1quh510x4UE0jynvvToWg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@sentry/types" "6.2.2"
|
"@sentry/types" "6.3.0"
|
||||||
tslib "^1.9.3"
|
tslib "^1.9.3"
|
||||||
|
|
||||||
"@sinonjs/commons@^1.7.0":
|
"@sinonjs/commons@^1.7.0":
|
||||||
@ -7192,6 +7202,11 @@ image-outline@^0.1.0:
|
|||||||
marching-squares "^0.2.0"
|
marching-squares "^0.2.0"
|
||||||
minimist "^1.2.0"
|
minimist "^1.2.0"
|
||||||
ndarray "^1.0.18"
|
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:
|
immer@8.0.1:
|
||||||
version "8.0.1"
|
version "8.0.1"
|
||||||
@ -7317,6 +7332,11 @@ internal-slot@^1.0.2:
|
|||||||
has "^1.0.3"
|
has "^1.0.3"
|
||||||
side-channel "^1.0.2"
|
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:
|
invariant@^2.2.2:
|
||||||
version "2.2.4"
|
version "2.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
|
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"
|
prelude-ls "~1.1.2"
|
||||||
type-check "~0.3.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:
|
line-column@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/line-column/-/line-column-1.0.2.tgz#d25af2936b6f4849172b312e4792d1d987bc34a2"
|
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"
|
emojis-list "^3.0.0"
|
||||||
json5 "^1.0.1"
|
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:
|
locate-path@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
|
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:
|
dependencies:
|
||||||
prop-types "^15.5.8"
|
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:
|
react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.6:
|
||||||
version "16.13.1"
|
version "16.13.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
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"
|
"@babel/runtime" "^7.3.1"
|
||||||
prop-types "^15.5.8"
|
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:
|
react-toast-notifications@^2.4.3:
|
||||||
version "2.4.3"
|
version "2.4.3"
|
||||||
resolved "https://registry.yarnpkg.com/react-toast-notifications/-/react-toast-notifications-2.4.3.tgz#ebf2ee776615a97906cef214352cfd9fe800c583"
|
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"
|
resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8"
|
||||||
integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==
|
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:
|
ts-pnp@1.2.0, ts-pnp@^1.1.6:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92"
|
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"
|
punycode "1.3.2"
|
||||||
querystring "0.2.0"
|
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:
|
use-image@^1.0.7:
|
||||||
version "1.0.7"
|
version "1.0.7"
|
||||||
resolved "https://registry.yarnpkg.com/use-image/-/use-image-1.0.7.tgz#b2c42b78d32868762a37631673eb169a244d5ad1"
|
resolved "https://registry.yarnpkg.com/use-image/-/use-image-1.0.7.tgz#b2c42b78d32868762a37631673eb169a244d5ad1"
|
||||||
integrity sha512-Z8hB8+lAGe25iqO3YaHO7bSvBSGErEakYQ6RGyRrPZoMDLKBIuZ67ikzn8f5ydjWorqFzeX+U3vVwnXoE1Q56Q==
|
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:
|
use@^3.1.0:
|
||||||
version "3.1.1"
|
version "3.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
|
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
|
||||||
|