Typescript

This commit is contained in:
Mitchell McCaffrey 2021-07-17 14:36:39 +10:00
parent fecf8090ea
commit 97734a2f55
25 changed files with 296 additions and 181 deletions

View File

@ -5,7 +5,7 @@ import { Data } from "@dnd-kit/core/dist/store/types";
type DraggableProps = {
id: string;
children: React.ReactNode;
data: Data;
data?: Data;
};
function Draggable({ id, children, data }: DraggableProps) {

View File

@ -259,11 +259,9 @@ function Map({
onMapTokenStateRemove(state);
setTokenDraggingOptions(undefined);
}}
onTokenStateChange={onMapTokenStateChange}
tokenState={tokenDraggingOptions && tokenDraggingOptions.tokenState}
tokenGroup={tokenDraggingOptions && tokenDraggingOptions.tokenGroup}
tokenNode={tokenDraggingOptions && tokenDraggingOptions.tokenNode}
dragging={!!(tokenDraggingOptions && tokenDraggingOptions.dragging)}
token={tokensById[tokenDraggingOptions.tokenState.tokenId]}
/>
);

View File

@ -89,7 +89,7 @@ function MapTokens({
setTokenDraggingOptions({
dragging: true,
tokenState,
tokenGroup: e.target,
tokenNode: e.target,
})
}
onTokenDragEnd={() =>

View File

@ -1,11 +1,18 @@
import React, { useState } from "react";
import { useState } from "react";
import { IconButton } from "theme-ui";
import SelectTokensIcon from "../../icons/SelectTokensIcon";
import SelectTokensModal from "../../modals/SelectTokensModal";
import { MapTokensStateCreateHandler } from "../../types/Events";
function SelectTokensButton({ onMapTokensStateCreate }) {
type SelectTokensButtonProps = {
onMapTokensStateCreate: MapTokensStateCreateHandler;
};
function SelectTokensButton({
onMapTokensStateCreate,
}: SelectTokensButtonProps) {
const [isModalOpen, setIsModalOpen] = useState(false);
function openModal() {
setIsModalOpen(true);
@ -14,9 +21,6 @@ function SelectTokensButton({ onMapTokensStateCreate }) {
setIsModalOpen(false);
}
function handleDone() {
closeModal();
}
return (
<>
<IconButton
@ -29,7 +33,6 @@ function SelectTokensButton({ onMapTokensStateCreate }) {
<SelectTokensModal
isOpen={isModalOpen}
onRequestClose={closeModal}
onDone={handleDone}
onMapTokensStateCreate={onMapTokensStateCreate}
/>
</>

View File

@ -1,4 +1,4 @@
import React, { useState } from "react";
import { useState } from "react";
import { createPortal } from "react-dom";
import { Box, Flex, Grid } from "theme-ui";
import SimpleBar from "simplebar-react";
@ -9,6 +9,7 @@ import {
KeyboardSensor,
useSensor,
useSensors,
DragStartEvent,
} from "@dnd-kit/core";
import TokenBarToken from "./TokenBarToken";
@ -23,7 +24,7 @@ import usePreventSelect from "../../hooks/usePreventSelect";
import { useTokenData } from "../../contexts/TokenDataContext";
import { useUserId } from "../../contexts/UserIdContext";
import { useMapStage } from "../../contexts/MapStageContext";
import DragContext from "../../contexts/DragContext";
import DragContext, { CustomDragEndEvent } from "../../contexts/DragContext";
import {
createTokenState,
@ -31,13 +32,19 @@ import {
} from "../../helpers/token";
import { findGroup } from "../../helpers/group";
import Vector2 from "../../helpers/Vector2";
import { MapTokensStateCreateHandler } from "../../types/Events";
import { Group } from "../../types/Group";
function TokenBar({ onMapTokensStateCreate }) {
type TokenBarProps = {
onMapTokensStateCreate: MapTokensStateCreateHandler;
};
function TokenBar({ onMapTokensStateCreate }: TokenBarProps) {
const userId = useUserId();
const { tokensById, tokenGroups } = useTokenData();
const [fullScreen] = useSetting("map.fullScreen");
const [fullScreen] = useSetting<boolean>("map.fullScreen");
const [dragId, setDragId] = useState();
const [dragId, setDragId] = useState<string | null>(null);
const mapStageRef = useMapStage();
@ -52,14 +59,20 @@ function TokenBar({ onMapTokensStateCreate }) {
const [preventSelect, resumeSelect] = usePreventSelect();
function handleDragStart({ active }) {
function handleDragStart({ active }: DragStartEvent) {
setDragId(active.id);
preventSelect();
}
function handleDragEnd({ active, overlayNodeClientRect }) {
function handleDragEnd({
active,
overlayNodeClientRect,
}: CustomDragEndEvent) {
setDragId(null);
resumeSelect();
if (!userId) {
return;
}
const mapStage = mapStageRef.current;
if (mapStage && overlayNodeClientRect) {
const dragRect = overlayNodeClientRect;
@ -96,8 +109,6 @@ function TokenBar({ onMapTokensStateCreate }) {
}
}
}
resumeSelect();
}
function handleDragCancel() {
@ -105,7 +116,7 @@ function TokenBar({ onMapTokensStateCreate }) {
resumeSelect();
}
function renderToken(group, draggable = true) {
function renderToken(group: Group, draggable = true) {
if (group.type === "item") {
const token = tokensById[group.id];
if (token && !token.hideInSidebar) {
@ -140,6 +151,8 @@ function TokenBar({ onMapTokensStateCreate }) {
}
}
const dragGroup = dragId && findGroup(tokenGroups, dragId);
return (
<DragContext
onDragStart={handleDragStart}
@ -188,7 +201,7 @@ function TokenBar({ onMapTokensStateCreate }) {
</Flex>
{createPortal(
<DragOverlay dropAnimation={null}>
{dragId && renderToken(findGroup(tokenGroups, dragId), false)}
{dragGroup && renderToken(dragGroup, false)}
</DragOverlay>,
document.body
)}

View File

@ -1,10 +1,15 @@
import React from "react";
import { Box } from "theme-ui";
import { useInView } from "react-intersection-observer";
import TokenImage from "./TokenImage";
function TokenBarToken({ token }) {
import { Token } from "../../types/Token";
type TokenBarTokenProps = {
token: Token;
};
function TokenBarToken({ token }: TokenBarTokenProps) {
const [ref, inView] = useInView({ triggerOnce: true });
return (

View File

@ -1,4 +1,4 @@
import React, { useState, useRef } from "react";
import { useState, useRef } from "react";
import { Grid, Flex, Box } from "theme-ui";
import { useSpring, animated } from "react-spring";
import { useDraggable } from "@dnd-kit/core";
@ -11,10 +11,22 @@ import Draggable from "../drag/Draggable";
import Vector2 from "../../helpers/Vector2";
import GroupIcon from "../../icons/GroupIcon";
import { GroupContainer } from "../../types/Group";
import { Token } from "../../types/Token";
function TokenBarTokenGroup({ group, tokens, draggable }) {
type TokenBarTokenGroupProps = {
group: GroupContainer;
tokens: Token[];
draggable: boolean;
};
function TokenBarTokenGroup({
group,
tokens,
draggable,
}: TokenBarTokenGroupProps) {
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
id: draggable && group.id,
id: group.id,
disabled: !draggable,
});
const [isOpen, setIsOpen] = useState(false);
@ -23,7 +35,7 @@ function TokenBarTokenGroup({ group, tokens, draggable }) {
height: isOpen ? (tokens.length + 1) * 56 : 56,
});
function renderToken(token) {
function renderToken(token: Token) {
if (draggable) {
return (
<Draggable id={token.id} key={token.id}>
@ -77,7 +89,6 @@ function TokenBarTokenGroup({ group, tokens, draggable }) {
gridTemplateRows: "1fr 1fr",
}}
p="2px"
alt={group.name}
title={group.name}
{...listeners}
{...attributes}
@ -100,10 +111,13 @@ function TokenBarTokenGroup({ group, tokens, draggable }) {
// Reject the opening of a group if the pointer has moved
const clickDownPositionRef = useRef(new Vector2(0, 0));
function handleOpenDown(event) {
function handleOpenDown(event: React.PointerEvent<HTMLDivElement>) {
clickDownPositionRef.current = new Vector2(event.clientX, event.clientY);
}
function handleOpenClick(event, newOpen) {
function handleOpenClick(
event: React.MouseEvent<HTMLDivElement>,
newOpen: boolean
) {
const clickPosition = new Vector2(event.clientX, event.clientY);
const distance = Vector2.distance(
clickPosition,

View File

@ -1,56 +0,0 @@
import React from "react";
import { useUserId } from "../../contexts/UserIdContext";
import {
useMapWidth,
useMapHeight,
} from "../../contexts/MapInteractionContext";
import DragOverlay from "../map/DragOverlay";
function TokenDragOverlay({
onTokenStateRemove,
onTokenStateChange,
token,
tokenState,
tokenGroup,
dragging,
}) {
const userId = useUserId();
const mapWidth = useMapWidth();
const mapHeight = useMapHeight();
function handleTokenRemove() {
// Handle other tokens when a vehicle gets deleted
if (token && token.category === "vehicle") {
const layer = tokenGroup.getLayer();
const mountedTokens = tokenGroup.find(".token");
for (let mountedToken of mountedTokens) {
// Save and restore token position after moving layer
const position = mountedToken.absolutePosition();
mountedToken.moveTo(layer);
mountedToken.absolutePosition(position);
onTokenStateChange({
[mountedToken.id()]: {
x: mountedToken.x() / mapWidth,
y: mountedToken.y() / mapHeight,
lastModifiedBy: userId,
lastModified: Date.now(),
},
});
}
}
onTokenStateRemove(tokenState);
}
return (
<DragOverlay
dragging={dragging}
onRemove={handleTokenRemove}
node={tokenGroup}
/>
);
}
export default TokenDragOverlay;

View File

@ -0,0 +1,33 @@
import Konva from "konva";
import DragOverlay from "../map/DragOverlay";
import { MapTokenStateRemoveHandler } from "../../types/Events";
import { TokenState } from "../../types/TokenState";
type TokenDragOverlayProps = {
onTokenStateRemove: MapTokenStateRemoveHandler;
tokenState: TokenState;
tokenNode: Konva.Node;
dragging: boolean;
};
function TokenDragOverlay({
onTokenStateRemove,
tokenState,
tokenNode,
dragging,
}: TokenDragOverlayProps) {
function handleTokenRemove() {
onTokenStateRemove(tokenState);
}
return (
<DragOverlay
dragging={dragging}
onRemove={handleTokenRemove}
node={tokenNode}
/>
);
}
export default TokenDragOverlay;

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import { useState, useEffect } from "react";
import { Flex, Close, IconButton } from "theme-ui";
import { groupsFromIds, itemsFromGroups } from "../../helpers/group";
@ -15,10 +15,15 @@ import { useKeyboard } from "../../contexts/KeyboardContext";
import shortcuts from "../../shortcuts";
function TokenEditBar({ disabled, onLoad }) {
type TokenEditBarProps = {
disabled: boolean;
onLoad: (load: boolean) => void;
};
function TokenEditBar({ disabled, onLoad }: TokenEditBarProps) {
const { tokens, removeTokens, updateTokensHidden } = useTokenData();
const { activeGroups, selectedGroupIds, onGroupSelect } = useGroup();
const { activeGroups, selectedGroupIds, onClearSelection } = useGroup();
const [allTokensVisible, setAllTokensVisisble] = useState(false);
@ -40,12 +45,12 @@ function TokenEditBar({ disabled, onLoad }) {
setIsTokensRemoveModalOpen(false);
const selectedTokens = getSelectedTokens();
const selectedTokenIds = selectedTokens.map((token) => token.id);
onGroupSelect();
onClearSelection();
await removeTokens(selectedTokenIds);
onLoad(false);
}
async function handleTokensHide(hideInSidebar) {
async function handleTokensHide(hideInSidebar: boolean) {
const selectedTokens = getSelectedTokens();
const selectedTokenIds = selectedTokens.map((token) => token.id);
// Show loading indicator if hiding more than 10 tokens
@ -61,7 +66,7 @@ function TokenEditBar({ disabled, onLoad }) {
/**
* Shortcuts
*/
function handleKeyDown(event) {
function handleKeyDown(event: KeyboardEvent) {
if (disabled) {
return;
}
@ -101,7 +106,7 @@ function TokenEditBar({ disabled, onLoad }) {
<Close
title="Clear Selection"
aria-label="Clear Selection"
onClick={() => onGroupSelect()}
onClick={() => onClearSelection()}
/>
<Flex>
<IconButton

View File

@ -1,10 +1,13 @@
import React from "react";
import { Flex } from "theme-ui";
import TokenShowIcon from "../../icons/TokenShowIcon";
import TokenHideIcon from "../../icons/TokenHideIcon";
function TokenHiddenBadge({ hidden }) {
type TokenHiddenBadgeProps = {
hidden: boolean;
};
function TokenHiddenBadge({ hidden }: TokenHiddenBadgeProps) {
return (
<Flex
sx={{

View File

@ -1,13 +1,18 @@
import React, { useState } from "react";
import { Image, Box } from "theme-ui";
import { useState } from "react";
import { Image, Box, ImageProps } from "theme-ui";
import { useDataURL } from "../../contexts/AssetsContext";
import { tokenSources as defaultTokenSources } from "../../tokens";
import { Token } from "../../types/Token";
import { TokenOutlineSVG } from "./TokenOutline";
const TokenImage = React.forwardRef(({ token, ...props }, ref) => {
type TokenImageProps = {
token: Token;
} & ImageProps;
function TokenImage({ token, ...props }: TokenImageProps) {
const tokenURL = useDataURL(
token,
defaultTokenSources,
@ -35,12 +40,11 @@ const TokenImage = React.forwardRef(({ token, ...props }, ref) => {
<Image
onLoad={() => setShowOutline(false)}
src={tokenURL}
ref={ref}
style={showOutline ? { display: "none" } : props.style}
{...props}
/>
</>
);
});
}
export default TokenImage;

View File

@ -1,13 +1,21 @@
import React, { useRef, useEffect, useState } from "react";
import Konva from "konva";
import { useRef, useEffect, useState } from "react";
import { Rect, Text, Group } from "react-konva";
import useSetting from "../../hooks/useSetting";
import { TokenState } from "../../types/TokenState";
const maxTokenSize = 3;
const defaultFontSize = 16;
function TokenLabel({ tokenState, width, height }) {
const [labelSize] = useSetting("map.labelSize");
type TokenLabelProps = {
tokenState: TokenState;
width: number;
height: number;
};
function TokenLabel({ tokenState, width, height }: TokenLabelProps) {
const [labelSize] = useSetting<number>("map.labelSize");
const paddingY =
(height / 12 / tokenState.size) * Math.min(tokenState.size, maxTokenSize);
@ -22,7 +30,7 @@ function TokenLabel({ tokenState, width, height }) {
return;
}
let fontSizes = [];
let fontSizes: number[] = [];
for (let size = 20 * labelSize; size >= 6; size--) {
const verticalSize = height / size / tokenState.size;
const tokenSize = Math.min(tokenState.size, maxTokenSize);
@ -30,7 +38,7 @@ function TokenLabel({ tokenState, width, height }) {
fontSizes.push(fontSize);
}
function findFontScale() {
const findFontScale = () => {
const size = fontSizes.reduce((prev, curr) => {
text.fontSize(curr);
const textWidth = text.getTextWidth() + paddingX * 2;
@ -42,7 +50,7 @@ function TokenLabel({ tokenState, width, height }) {
}, 1);
setFontScale(size / defaultFontSize);
}
};
findFontScale();
}, [
@ -68,8 +76,8 @@ function TokenLabel({ tokenState, width, height }) {
}
}, [tokenState.label, paddingX, width, fontScale]);
const textRef = useRef();
const textSizerRef = useRef();
const textRef = useRef<Konva.Text>(null);
const textSizerRef = useRef<Konva.Text>(null);
return (
<Group y={height - (defaultFontSize * fontScale + paddingY) / 2}>

View File

@ -1,5 +1,6 @@
import React, { useEffect, useState } from "react";
import { Box, Input, Flex, Text, IconButton } from "theme-ui";
import Konva from "konva";
import Slider from "../Slider";
@ -16,6 +17,22 @@ import HideIcon from "../../icons/TokenHideIcon";
import { useUserId } from "../../contexts/UserIdContext";
import {
RequestCloseEventHandler,
TokenStateChangeEventHandler,
} from "../../types/Events";
import { TokenState } from "../../types/TokenState";
import { Map } from "../../types/Map";
type TokenMenuProps = {
isOpen: boolean;
onRequestClose: RequestCloseEventHandler;
tokenState: TokenState;
tokenImage: Konva.Node;
onTokenStateChange: TokenStateChangeEventHandler;
map: Map;
};
const defaultTokenMaxSize = 6;
function TokenMenu({
isOpen,
@ -24,7 +41,7 @@ function TokenMenu({
tokenImage,
onTokenStateChange,
map,
}) {
}: TokenMenuProps) {
const userId = useUserId();
const wasOpen = usePrevious(isOpen);
@ -39,22 +56,25 @@ function TokenMenu({
if (tokenImage) {
const imageRect = tokenImage.getClientRect();
const mapElement = document.querySelector(".map");
const mapRect = mapElement.getBoundingClientRect();
// Center X for the menu which is 156px wide
setMenuLeft(mapRect.left + imageRect.x + imageRect.width / 2 - 156 / 2);
// Y 12px from the bottom
setMenuTop(mapRect.top + imageRect.y + imageRect.height + 12);
if (mapElement) {
const mapRect = mapElement.getBoundingClientRect();
// Center X for the menu which is 156px wide
setMenuLeft(
mapRect.left + imageRect.x + imageRect.width / 2 - 156 / 2
);
// Y 12px from the bottom
setMenuTop(mapRect.top + imageRect.y + imageRect.height + 12);
}
}
}
}, [isOpen, tokenState, wasOpen, tokenImage]);
function handleLabelChange(event) {
function handleLabelChange(event: React.ChangeEvent<HTMLInputElement>) {
const label = event.target.value.substring(0, 48);
tokenState && onTokenStateChange({ [tokenState.id]: { label: label } });
}
function handleStatusChange(status) {
function handleStatusChange(status: string) {
if (!tokenState) {
return;
}
@ -69,12 +89,12 @@ function TokenMenu({
});
}
function handleSizeChange(event) {
function handleSizeChange(event: React.ChangeEvent<HTMLInputElement>) {
const newSize = parseFloat(event.target.value);
tokenState && onTokenStateChange({ [tokenState.id]: { size: newSize } });
}
function handleRotationChange(event) {
function handleRotationChange(event: React.ChangeEvent<HTMLInputElement>) {
const newRotation = parseInt(event.target.value);
tokenState &&
onTokenStateChange({
@ -96,26 +116,31 @@ function TokenMenu({
});
}
function handleModalContent(node) {
function handleModalContent(node: HTMLElement) {
if (node) {
// Focus input
const tokenLabelInput = node.querySelector("#changeTokenLabel");
tokenLabelInput.focus();
tokenLabelInput.select();
const tokenLabelInput =
node.querySelector<HTMLInputElement>("#changeTokenLabel");
if (tokenLabelInput) {
tokenLabelInput.focus();
tokenLabelInput.select();
}
// Ensure menu is in bounds
const nodeRect = node.getBoundingClientRect();
const mapElement = document.querySelector(".map");
const mapRect = mapElement.getBoundingClientRect();
setMenuLeft((prevLeft) =>
Math.min(
mapRect.right - nodeRect.width,
Math.max(mapRect.left, prevLeft)
)
);
setMenuTop((prevTop) =>
Math.min(mapRect.bottom - nodeRect.height, prevTop)
);
if (mapElement) {
const mapRect = mapElement.getBoundingClientRect();
setMenuLeft((prevLeft) =>
Math.min(
mapRect.right - nodeRect.width,
Math.max(mapRect.left, prevLeft)
)
);
setMenuTop((prevTop) =>
Math.min(mapRect.bottom - nodeRect.height, prevTop)
);
}
}
}

View File

@ -1,9 +1,19 @@
import React from "react";
import { Rect, Circle, Line } from "react-konva";
import colors from "../../helpers/colors";
import { Outline } from "../../types/Outline";
export function TokenOutlineSVG({ outline, width, height }) {
type TokenOutlineSVGProps = {
outline: Outline;
width: number;
height: number;
};
export function TokenOutlineSVG({
outline,
width,
height,
}: TokenOutlineSVGProps) {
if (outline.type === "rect") {
return (
<svg
@ -55,7 +65,12 @@ export function TokenOutlineSVG({ outline, width, height }) {
}
}
function TokenOutline({ outline, hidden }) {
type TokenOutlineProps = {
outline: Outline;
hidden: boolean;
};
function TokenOutline({ outline, hidden }: TokenOutlineProps) {
const sharedProps = {
fill: colors.black,
opacity: hidden ? 0 : 0.8,
@ -84,7 +99,7 @@ function TokenOutline({ outline, hidden }) {
<Line
points={outline.points}
closed
tension={outline.points < 200 ? 0 : 0.33}
tension={outline.points.length < 200 ? 0 : 0.33}
{...sharedProps}
/>
);

View File

@ -1,8 +1,9 @@
import React, { useState, useRef, useEffect } from "react";
import { useState, useRef } from "react";
import { Box, IconButton } from "theme-ui";
import { Stage, Layer, Image, Rect, Group } from "react-konva";
import ReactResizeDetector from "react-resize-detector";
import useImage from "use-image";
import Konva from "konva";
import usePreventOverscroll from "../../hooks/usePreventOverscroll";
import useStageInteraction from "../../hooks/useStageInteraction";
@ -18,32 +19,32 @@ import GridOffIcon from "../../icons/GridOffIcon";
import { tokenSources } from "../../tokens";
import Grid from "../Grid";
import { Token } from "../../types/Token";
function TokenPreview({ token }) {
const [tokenSourceData, setTokenSourceData] = useState({});
useEffect(() => {
if (token.id !== tokenSourceData.id) {
setTokenSourceData(token);
}
}, [token, tokenSourceData]);
type TokenPreviewProps = {
token: Token;
};
const tokenURL = useDataURL(tokenSourceData, tokenSources);
const [tokenSourceImage] = useImage(tokenURL);
function TokenPreview({ token }: TokenPreviewProps) {
const tokenURL = useDataURL(token, tokenSources);
const [tokenSourceImage] = useImage(tokenURL || "");
const [stageWidth, setStageWidth] = useState(1);
const [stageHeight, setStageHeight] = useState(1);
const [stageScale, setStageScale] = useState(1);
const stageTranslateRef = useRef({ x: 0, y: 0 });
const tokenStageRef = useRef();
const tokenLayerRef = useRef();
const tokenStageRef = useRef<Konva.Stage>(null);
const tokenLayerRef = useRef<Konva.Layer>(null);
function handleResize(width, height) {
setStageWidth(width);
setStageHeight(height);
function handleResize(width?: number, height?: number) {
if (width && height) {
setStageWidth(width);
setStageHeight(height);
}
}
const containerRef = useRef();
const containerRef = useRef<HTMLDivElement>(null);
usePreventOverscroll(containerRef);
const [tokenWidth, tokenHeight] = useImageCenter(
@ -59,11 +60,11 @@ function TokenPreview({ token }) {
);
useStageInteraction(
tokenStageRef.current,
tokenStageRef,
stageScale,
setStageScale,
stageTranslateRef,
tokenLayerRef.current
tokenLayerRef
);
const [showGridPreview, setShowGridPreview] = useState(true);

View File

@ -1,17 +1,25 @@
import React from "react";
import { Flex, Box, Input, Label } from "theme-ui";
import { isEmpty } from "../../helpers/shared";
import Select from "../Select";
const categorySettings = [
import { Token, TokenCategory } from "../../types/Token";
import { TokenSettingsChangeEventHandler } from "../../types/Events";
type CategorySetting = { value: TokenCategory; label: string };
const categorySettings: CategorySetting[] = [
{ value: "character", label: "Character" },
{ value: "prop", label: "Prop" },
{ value: "vehicle", label: "Vehicle / Mount" },
];
function TokenSettings({ token, onSettingsChange }) {
type TokenSettingsProps = {
token: Token;
onSettingsChange: TokenSettingsChangeEventHandler;
};
function TokenSettings({ token, onSettingsChange }: TokenSettingsProps) {
const tokenEmpty = !token || isEmpty(token);
return (
<Flex sx={{ flexDirection: "column" }}>
@ -20,7 +28,7 @@ function TokenSettings({ token, onSettingsChange }) {
<Input
name="name"
value={(token && token.name) || ""}
onChange={(e) => onSettingsChange("name", e.target.value)}
onChange={(e) => onSettingsChange({ name: e.target.value })}
disabled={tokenEmpty}
my={1}
/>
@ -30,12 +38,14 @@ function TokenSettings({ token, onSettingsChange }) {
<Select
options={categorySettings}
value={
!tokenEmpty &&
categorySettings.find((s) => s.value === token.defaultCategory)
tokenEmpty
? undefined
: categorySettings.find((s) => s.value === token.defaultCategory)
}
isDisabled={tokenEmpty}
onChange={(option) =>
onSettingsChange("defaultCategory", option.value)
onChange={
((option: CategorySetting) =>
onSettingsChange({ defaultCategory: option.value })) as any
}
isSearchable={false}
/>
@ -47,7 +57,7 @@ function TokenSettings({ token, onSettingsChange }) {
name="tokenSize"
value={`${(token && token.defaultSize) || 0}`}
onChange={(e) =>
onSettingsChange("defaultSize", parseFloat(e.target.value))
onSettingsChange({ defaultSize: parseFloat(e.target.value) })
}
disabled={tokenEmpty}
min={1}
@ -59,7 +69,7 @@ function TokenSettings({ token, onSettingsChange }) {
<Input
name="label"
value={(token && token.defaultLabel) || ""}
onChange={(e) => onSettingsChange("defaultLabel", e.target.value)}
onChange={(e) => onSettingsChange({ defaultLabel: e.target.value })}
disabled={tokenEmpty}
my={1}
/>

View File

@ -1,9 +1,15 @@
import React from "react";
import { Circle, Group } from "react-konva";
import colors from "../../helpers/colors";
import { TokenState } from "../../types/TokenState";
function TokenStatus({ tokenState, width, height }) {
type TokenStatusProps = {
tokenState: TokenState;
width: number;
height: number;
};
function TokenStatus({ tokenState, width, height }: TokenStatusProps) {
// Ensure statuses is an array and filter empty values
const statuses = [...new Set((tokenState?.statuses || []).filter((s) => s))];
return (

View File

@ -1,8 +1,18 @@
import React from "react";
import { Token } from "../../types/Token";
import Tile from "../tile/Tile";
import TokenImage from "./TokenImage";
type TokenTileProps = {
token: Token;
isSelected: boolean;
onSelect: (tokenId: string) => void;
onTokenEdit: (tokenId: string) => void;
canEdit: boolean;
badges: React.ReactChild[];
};
function TokenTile({
token,
isSelected,
@ -10,7 +20,7 @@ function TokenTile({
onTokenEdit,
canEdit,
badges,
}) {
}: TokenTileProps) {
return (
<Tile
title={token.name}

View File

@ -1,10 +1,19 @@
import React from "react";
import { Grid } from "theme-ui";
import Tile from "../tile/Tile";
import TokenImage from "./TokenImage";
import useResponsiveLayout from "../../hooks/useResponsiveLayout";
import { GroupContainer } from "../../types/Group";
import { Token } from "../../types/Token";
type TokenTileProps = {
group: GroupContainer;
tokens: Token[];
isSelected: boolean;
onSelect: (tokenId: string) => void;
onDoubleClick: () => void;
};
function TokenTileGroup({
group,
@ -12,7 +21,7 @@ function TokenTileGroup({
isSelected,
onSelect,
onDoubleClick,
}) {
}: TokenTileProps) {
const layout = useResponsiveLayout();
return (

View File

@ -1,5 +1,3 @@
import React from "react";
import TokenTile from "./TokenTile";
import TokenTileGroup from "./TokenTileGroup";
import TokenHiddenBadge from "./TokenHiddenBadge";
@ -10,12 +8,20 @@ import SortableTilesDragOverlay from "../tile/SortableTilesDragOverlay";
import { getGroupItems } from "../../helpers/group";
import { useGroup } from "../../contexts/GroupContext";
import { Token } from "../../types/Token";
import { Group } from "../../types/Group";
function TokenTiles({ tokensById, onTokenEdit, subgroup }) {
type TokenTilesProps = {
tokensById: Record<string, Token>;
onTokenEdit: (tokenId: string) => void;
subgroup: boolean;
};
function TokenTiles({ tokensById, onTokenEdit, subgroup }: TokenTilesProps) {
const { selectedGroupIds, selectMode, onGroupOpen, onGroupSelect } =
useGroup();
function renderTile(group) {
function renderTile(group: Group) {
if (group.type === "item") {
const token = tokensById[group.id];
if (token) {

View File

@ -44,10 +44,10 @@ function EditTokenModal({
>({});
// TODO: CHANGE MAP BACK? OR CHANGE THIS TO PARTIAL
function handleTokenSettingsChange(key: string, value: Pick<Token, any>) {
function handleTokenSettingsChange(change: Partial<Token>) {
setTokenSettingChanges((prevChanges) => ({
...prevChanges,
[key]: value,
...change,
}));
}
@ -67,7 +67,7 @@ function EditTokenModal({
const selectedTokenWithChanges = {
...token,
...tokenSettingChanges,
};
} as Token;
const layout = useResponsiveLayout();

View File

@ -3,6 +3,7 @@ import { DefaultDice } from "./Dice";
import { Map } from "./Map";
import { MapState } from "./MapState";
import { Note } from "./Note";
import { Token } from "./Token";
import { TokenState } from "./TokenState";
export type MapChangeEventHandler = (map?: Map, mapState?: MapState) => void;
@ -26,6 +27,7 @@ export type TokenMenuOpenChangeEventHandler = (
tokenStateId: string,
tokenImage: Konva.Node
) => void;
export type TokenSettingsChangeEventHandler = (change: Partial<Token>) => void;
export type NoteAddEventHander = (note: Note) => void;
export type NoteRemoveEventHander = (noteId: string) => void;

View File

@ -40,5 +40,5 @@ export type TokenMenuOptions = {
export type TokenDraggingOptions = {
dragging: boolean;
tokenState: TokenState;
tokenGroup: Konva.Node;
tokenNode: Konva.Node;
};

View File

@ -1,3 +1,4 @@
import { Color } from "../helpers/colors";
import { Outline } from "./Outline";
import { TokenCategory } from "./Token";
@ -8,7 +9,7 @@ export type BaseTokenState = {
size: number;
category: TokenCategory;
label: string;
statuses: string[];
statuses: Color[];
x: number;
y: number;
lastModifiedBy: string;