Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4fb69dbc7 |
9
.jsinspectrc
Normal file
9
.jsinspectrc
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"threshold": 50,
|
||||||
|
"minInstances": 3,
|
||||||
|
"identifiers": false,
|
||||||
|
"literals": false,
|
||||||
|
"ignore": "dist|__tests__|node_modules",
|
||||||
|
"reporter": "default",
|
||||||
|
"truncate": 100
|
||||||
|
}
|
||||||
@@ -3,66 +3,17 @@ import {
|
|||||||
getConditionDescription,
|
getConditionDescription,
|
||||||
getConditionsForEdition,
|
getConditionsForEdition,
|
||||||
} from "@initiative/domain";
|
} from "@initiative/domain";
|
||||||
import type { LucideIcon } from "lucide-react";
|
import { useLayoutEffect, useRef, useState } from "react";
|
||||||
import {
|
|
||||||
ArrowDown,
|
|
||||||
Ban,
|
|
||||||
BatteryLow,
|
|
||||||
Droplet,
|
|
||||||
EarOff,
|
|
||||||
EyeOff,
|
|
||||||
Gem,
|
|
||||||
Ghost,
|
|
||||||
Hand,
|
|
||||||
Heart,
|
|
||||||
Link,
|
|
||||||
Moon,
|
|
||||||
ShieldMinus,
|
|
||||||
Siren,
|
|
||||||
Snail,
|
|
||||||
Sparkles,
|
|
||||||
ZapOff,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { useRulesEditionContext } from "../contexts/rules-edition-context.js";
|
import { useRulesEditionContext } from "../contexts/rules-edition-context.js";
|
||||||
|
import { useClickOutside } from "../hooks/use-click-outside.js";
|
||||||
import { cn } from "../lib/utils";
|
import { cn } from "../lib/utils";
|
||||||
|
import {
|
||||||
|
CONDITION_COLOR_CLASSES,
|
||||||
|
CONDITION_ICON_MAP,
|
||||||
|
} from "./condition-styles.js";
|
||||||
import { Tooltip } from "./ui/tooltip.js";
|
import { Tooltip } from "./ui/tooltip.js";
|
||||||
|
|
||||||
const ICON_MAP: Record<string, LucideIcon> = {
|
|
||||||
EyeOff,
|
|
||||||
Heart,
|
|
||||||
EarOff,
|
|
||||||
BatteryLow,
|
|
||||||
Siren,
|
|
||||||
Hand,
|
|
||||||
Ban,
|
|
||||||
Ghost,
|
|
||||||
ZapOff,
|
|
||||||
Gem,
|
|
||||||
Droplet,
|
|
||||||
ArrowDown,
|
|
||||||
Link,
|
|
||||||
ShieldMinus,
|
|
||||||
Snail,
|
|
||||||
Sparkles,
|
|
||||||
Moon,
|
|
||||||
};
|
|
||||||
|
|
||||||
const COLOR_CLASSES: Record<string, string> = {
|
|
||||||
neutral: "text-muted-foreground",
|
|
||||||
pink: "text-pink-400",
|
|
||||||
amber: "text-amber-400",
|
|
||||||
orange: "text-orange-400",
|
|
||||||
gray: "text-gray-400",
|
|
||||||
violet: "text-violet-400",
|
|
||||||
yellow: "text-yellow-400",
|
|
||||||
slate: "text-slate-400",
|
|
||||||
green: "text-green-400",
|
|
||||||
indigo: "text-indigo-400",
|
|
||||||
sky: "text-sky-400",
|
|
||||||
};
|
|
||||||
|
|
||||||
interface ConditionPickerProps {
|
interface ConditionPickerProps {
|
||||||
anchorRef: React.RefObject<HTMLElement | null>;
|
anchorRef: React.RefObject<HTMLElement | null>;
|
||||||
activeConditions: readonly ConditionId[] | undefined;
|
activeConditions: readonly ConditionId[] | undefined;
|
||||||
@@ -104,15 +55,7 @@ export function ConditionPicker({
|
|||||||
setPos({ top, left: anchorRect.left, maxHeight });
|
setPos({ top, left: anchorRect.left, maxHeight });
|
||||||
}, [anchorRef]);
|
}, [anchorRef]);
|
||||||
|
|
||||||
useEffect(() => {
|
useClickOutside(ref, onClose);
|
||||||
function handleClickOutside(e: MouseEvent) {
|
|
||||||
if (ref.current && !ref.current.contains(e.target as Node)) {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
document.addEventListener("mousedown", handleClickOutside);
|
|
||||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
||||||
}, [onClose]);
|
|
||||||
|
|
||||||
const { edition } = useRulesEditionContext();
|
const { edition } = useRulesEditionContext();
|
||||||
const conditions = getConditionsForEdition(edition);
|
const conditions = getConditionsForEdition(edition);
|
||||||
@@ -129,10 +72,11 @@ export function ConditionPicker({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{conditions.map((def) => {
|
{conditions.map((def) => {
|
||||||
const Icon = ICON_MAP[def.iconName];
|
const Icon = CONDITION_ICON_MAP[def.iconName];
|
||||||
if (!Icon) return null;
|
if (!Icon) return null;
|
||||||
const isActive = active.has(def.id);
|
const isActive = active.has(def.id);
|
||||||
const colorClass = COLOR_CLASSES[def.color] ?? "text-muted-foreground";
|
const colorClass =
|
||||||
|
CONDITION_COLOR_CLASSES[def.color] ?? "text-muted-foreground";
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
key={def.id}
|
key={def.id}
|
||||||
|
|||||||
54
apps/web/src/components/condition-styles.ts
Normal file
54
apps/web/src/components/condition-styles.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
import {
|
||||||
|
ArrowDown,
|
||||||
|
Ban,
|
||||||
|
BatteryLow,
|
||||||
|
Droplet,
|
||||||
|
EarOff,
|
||||||
|
EyeOff,
|
||||||
|
Gem,
|
||||||
|
Ghost,
|
||||||
|
Hand,
|
||||||
|
Heart,
|
||||||
|
Link,
|
||||||
|
Moon,
|
||||||
|
ShieldMinus,
|
||||||
|
Siren,
|
||||||
|
Snail,
|
||||||
|
Sparkles,
|
||||||
|
ZapOff,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
export const CONDITION_ICON_MAP: Record<string, LucideIcon> = {
|
||||||
|
EyeOff,
|
||||||
|
Heart,
|
||||||
|
EarOff,
|
||||||
|
BatteryLow,
|
||||||
|
Siren,
|
||||||
|
Hand,
|
||||||
|
Ban,
|
||||||
|
Ghost,
|
||||||
|
ZapOff,
|
||||||
|
Gem,
|
||||||
|
Droplet,
|
||||||
|
ArrowDown,
|
||||||
|
Link,
|
||||||
|
ShieldMinus,
|
||||||
|
Snail,
|
||||||
|
Sparkles,
|
||||||
|
Moon,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CONDITION_COLOR_CLASSES: Record<string, string> = {
|
||||||
|
neutral: "text-muted-foreground",
|
||||||
|
pink: "text-pink-400",
|
||||||
|
amber: "text-amber-400",
|
||||||
|
orange: "text-orange-400",
|
||||||
|
gray: "text-gray-400",
|
||||||
|
violet: "text-violet-400",
|
||||||
|
yellow: "text-yellow-400",
|
||||||
|
slate: "text-slate-400",
|
||||||
|
green: "text-green-400",
|
||||||
|
indigo: "text-indigo-400",
|
||||||
|
sky: "text-sky-400",
|
||||||
|
};
|
||||||
@@ -3,65 +3,15 @@ import {
|
|||||||
type ConditionId,
|
type ConditionId,
|
||||||
getConditionDescription,
|
getConditionDescription,
|
||||||
} from "@initiative/domain";
|
} from "@initiative/domain";
|
||||||
import type { LucideIcon } from "lucide-react";
|
import { Plus } from "lucide-react";
|
||||||
import {
|
|
||||||
ArrowDown,
|
|
||||||
Ban,
|
|
||||||
BatteryLow,
|
|
||||||
Droplet,
|
|
||||||
EarOff,
|
|
||||||
EyeOff,
|
|
||||||
Gem,
|
|
||||||
Ghost,
|
|
||||||
Hand,
|
|
||||||
Heart,
|
|
||||||
Link,
|
|
||||||
Moon,
|
|
||||||
Plus,
|
|
||||||
ShieldMinus,
|
|
||||||
Siren,
|
|
||||||
Snail,
|
|
||||||
Sparkles,
|
|
||||||
ZapOff,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useRulesEditionContext } from "../contexts/rules-edition-context.js";
|
import { useRulesEditionContext } from "../contexts/rules-edition-context.js";
|
||||||
import { cn } from "../lib/utils.js";
|
import { cn } from "../lib/utils.js";
|
||||||
|
import {
|
||||||
|
CONDITION_COLOR_CLASSES,
|
||||||
|
CONDITION_ICON_MAP,
|
||||||
|
} from "./condition-styles.js";
|
||||||
import { Tooltip } from "./ui/tooltip.js";
|
import { Tooltip } from "./ui/tooltip.js";
|
||||||
|
|
||||||
const ICON_MAP: Record<string, LucideIcon> = {
|
|
||||||
EyeOff,
|
|
||||||
Heart,
|
|
||||||
EarOff,
|
|
||||||
BatteryLow,
|
|
||||||
Siren,
|
|
||||||
Hand,
|
|
||||||
Ban,
|
|
||||||
Ghost,
|
|
||||||
ZapOff,
|
|
||||||
Gem,
|
|
||||||
Droplet,
|
|
||||||
ArrowDown,
|
|
||||||
Link,
|
|
||||||
ShieldMinus,
|
|
||||||
Snail,
|
|
||||||
Sparkles,
|
|
||||||
Moon,
|
|
||||||
};
|
|
||||||
|
|
||||||
const COLOR_CLASSES: Record<string, string> = {
|
|
||||||
neutral: "text-muted-foreground",
|
|
||||||
pink: "text-pink-400",
|
|
||||||
amber: "text-amber-400",
|
|
||||||
orange: "text-orange-400",
|
|
||||||
gray: "text-gray-400",
|
|
||||||
violet: "text-violet-400",
|
|
||||||
yellow: "text-yellow-400",
|
|
||||||
slate: "text-slate-400",
|
|
||||||
green: "text-green-400",
|
|
||||||
indigo: "text-indigo-400",
|
|
||||||
sky: "text-sky-400",
|
|
||||||
};
|
|
||||||
|
|
||||||
interface ConditionTagsProps {
|
interface ConditionTagsProps {
|
||||||
conditions: readonly ConditionId[] | undefined;
|
conditions: readonly ConditionId[] | undefined;
|
||||||
onRemove: (conditionId: ConditionId) => void;
|
onRemove: (conditionId: ConditionId) => void;
|
||||||
@@ -79,9 +29,10 @@ export function ConditionTags({
|
|||||||
{conditions?.map((condId) => {
|
{conditions?.map((condId) => {
|
||||||
const def = CONDITION_DEFINITIONS.find((d) => d.id === condId);
|
const def = CONDITION_DEFINITIONS.find((d) => d.id === condId);
|
||||||
if (!def) return null;
|
if (!def) return null;
|
||||||
const Icon = ICON_MAP[def.iconName];
|
const Icon = CONDITION_ICON_MAP[def.iconName];
|
||||||
if (!Icon) return null;
|
if (!Icon) return null;
|
||||||
const colorClass = COLOR_CLASSES[def.color] ?? "text-muted-foreground";
|
const colorClass =
|
||||||
|
CONDITION_COLOR_CLASSES[def.color] ?? "text-muted-foreground";
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
key={condId}
|
key={condId}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Check, ClipboardCopy, Download, X } from "lucide-react";
|
import { Check, ClipboardCopy, Download } from "lucide-react";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { Button } from "./ui/button.js";
|
import { Dialog, DialogHeader } from "./ui/dialog.js";
|
||||||
import { Dialog } from "./ui/dialog.js";
|
|
||||||
import { Input } from "./ui/input.js";
|
import { Input } from "./ui/input.js";
|
||||||
|
|
||||||
interface ExportMethodDialogProps {
|
interface ExportMethodDialogProps {
|
||||||
@@ -30,18 +29,7 @@ export function ExportMethodDialog({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onClose={handleClose} className="w-80">
|
<Dialog open={open} onClose={handleClose} className="w-80">
|
||||||
<div className="mb-4 flex items-center justify-between">
|
<DialogHeader title="Export Encounter" onClose={handleClose} />
|
||||||
<h2 className="font-semibold text-lg">Export Encounter</h2>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon-sm"
|
|
||||||
onClick={handleClose}
|
|
||||||
className="text-muted-foreground"
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
import { useClickOutside } from "../hooks/use-click-outside.js";
|
||||||
import { Input } from "./ui/input";
|
import { Input } from "./ui/input";
|
||||||
|
|
||||||
const DIGITS_ONLY_REGEX = /^\d+$/;
|
const DIGITS_ONLY_REGEX = /^\d+$/;
|
||||||
@@ -48,15 +49,7 @@ export function HpAdjustPopover({
|
|||||||
requestAnimationFrame(() => inputRef.current?.focus());
|
requestAnimationFrame(() => inputRef.current?.focus());
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useClickOutside(ref, onClose);
|
||||||
function handleClickOutside(e: MouseEvent) {
|
|
||||||
if (ref.current && !ref.current.contains(e.target as Node)) {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
document.addEventListener("mousedown", handleClickOutside);
|
|
||||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
||||||
}, [onClose]);
|
|
||||||
|
|
||||||
const parsedValue =
|
const parsedValue =
|
||||||
inputValue === "" ? null : Number.parseInt(inputValue, 10);
|
inputValue === "" ? null : Number.parseInt(inputValue, 10);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ClipboardPaste, FileUp, X } from "lucide-react";
|
import { ClipboardPaste, FileUp } from "lucide-react";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { Button } from "./ui/button.js";
|
import { Button } from "./ui/button.js";
|
||||||
import { Dialog } from "./ui/dialog.js";
|
import { Dialog, DialogHeader } from "./ui/dialog.js";
|
||||||
|
|
||||||
interface ImportMethodDialogProps {
|
interface ImportMethodDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -41,18 +41,7 @@ export function ImportMethodDialog({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onClose={handleClose} className="w-80">
|
<Dialog open={open} onClose={handleClose} className="w-80">
|
||||||
<div className="mb-4 flex items-center justify-between">
|
<DialogHeader title="Import Encounter" onClose={handleClose} />
|
||||||
<h2 className="font-semibold text-lg">Import Encounter</h2>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon-sm"
|
|
||||||
onClick={handleClose}
|
|
||||||
className="text-muted-foreground"
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{mode === "pick" && (
|
{mode === "pick" && (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { PlayerCharacter, PlayerCharacterId } from "@initiative/domain";
|
import type { PlayerCharacter, PlayerCharacterId } from "@initiative/domain";
|
||||||
import { Pencil, Plus, Trash2, X } from "lucide-react";
|
import { Pencil, Plus, Trash2 } from "lucide-react";
|
||||||
import { PLAYER_COLOR_HEX, PLAYER_ICON_MAP } from "./player-icon-map";
|
import { PLAYER_COLOR_HEX, PLAYER_ICON_MAP } from "./player-icon-map";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
import { ConfirmButton } from "./ui/confirm-button";
|
import { ConfirmButton } from "./ui/confirm-button";
|
||||||
import { Dialog } from "./ui/dialog";
|
import { Dialog, DialogHeader } from "./ui/dialog";
|
||||||
|
|
||||||
interface PlayerManagementProps {
|
interface PlayerManagementProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -24,19 +24,7 @@ export function PlayerManagement({
|
|||||||
}: Readonly<PlayerManagementProps>) {
|
}: Readonly<PlayerManagementProps>) {
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onClose={onClose} className="card-glow w-full max-w-md">
|
<Dialog open={open} onClose={onClose} className="card-glow w-full max-w-md">
|
||||||
<div className="mb-4 flex items-center justify-between">
|
<DialogHeader title="Player Characters" onClose={onClose} />
|
||||||
<h2 className="font-semibold text-foreground text-lg">
|
|
||||||
Player Characters
|
|
||||||
</h2>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={onClose}
|
|
||||||
className="text-muted-foreground"
|
|
||||||
>
|
|
||||||
<X size={20} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{characters.length === 0 ? (
|
{characters.length === 0 ? (
|
||||||
<div className="flex flex-col items-center gap-3 py-8 text-center">
|
<div className="flex flex-col items-center gap-3 py-8 text-center">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { RollMode } from "@initiative/domain";
|
import type { RollMode } from "@initiative/domain";
|
||||||
import { ChevronsDown, ChevronsUp } from "lucide-react";
|
import { ChevronsDown, ChevronsUp } from "lucide-react";
|
||||||
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
import { useLayoutEffect, useRef, useState } from "react";
|
||||||
|
import { useClickOutside } from "../hooks/use-click-outside.js";
|
||||||
|
|
||||||
interface RollModeMenuProps {
|
interface RollModeMenuProps {
|
||||||
readonly position: { x: number; y: number };
|
readonly position: { x: number; y: number };
|
||||||
@@ -34,22 +35,7 @@ export function RollModeMenu({
|
|||||||
setPos({ top, left });
|
setPos({ top, left });
|
||||||
}, [position.x, position.y]);
|
}, [position.x, position.y]);
|
||||||
|
|
||||||
useEffect(() => {
|
useClickOutside(ref, onClose);
|
||||||
function handleMouseDown(e: MouseEvent) {
|
|
||||||
if (ref.current && !ref.current.contains(e.target as Node)) {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function handleKeyDown(e: KeyboardEvent) {
|
|
||||||
if (e.key === "Escape") onClose();
|
|
||||||
}
|
|
||||||
document.addEventListener("mousedown", handleMouseDown);
|
|
||||||
document.addEventListener("keydown", handleKeyDown);
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener("mousedown", handleMouseDown);
|
|
||||||
document.removeEventListener("keydown", handleKeyDown);
|
|
||||||
};
|
|
||||||
}, [onClose]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import type { RulesEdition } from "@initiative/domain";
|
import type { RulesEdition } from "@initiative/domain";
|
||||||
import { Monitor, Moon, Sun, X } from "lucide-react";
|
import { Monitor, Moon, Sun } from "lucide-react";
|
||||||
import { useRulesEditionContext } from "../contexts/rules-edition-context.js";
|
import { useRulesEditionContext } from "../contexts/rules-edition-context.js";
|
||||||
import { useThemeContext } from "../contexts/theme-context.js";
|
import { useThemeContext } from "../contexts/theme-context.js";
|
||||||
import { cn } from "../lib/utils.js";
|
import { cn } from "../lib/utils.js";
|
||||||
import { Button } from "./ui/button.js";
|
import { Dialog, DialogHeader } from "./ui/dialog.js";
|
||||||
import { Dialog } from "./ui/dialog.js";
|
|
||||||
|
|
||||||
interface SettingsModalProps {
|
interface SettingsModalProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -32,17 +31,7 @@ export function SettingsModal({ open, onClose }: Readonly<SettingsModalProps>) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onClose={onClose} className="card-glow w-full max-w-sm">
|
<Dialog open={open} onClose={onClose} className="card-glow w-full max-w-sm">
|
||||||
<div className="mb-4 flex items-center justify-between">
|
<DialogHeader title="Settings" onClose={onClose} />
|
||||||
<h2 className="font-semibold text-foreground text-lg">Settings</h2>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={onClose}
|
|
||||||
className="text-muted-foreground"
|
|
||||||
>
|
|
||||||
<X size={20} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-5">
|
<div className="flex flex-col gap-5">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -34,6 +34,31 @@ function SectionDivider() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function TraitSection({
|
||||||
|
entries,
|
||||||
|
heading,
|
||||||
|
}: Readonly<{
|
||||||
|
entries: readonly { name: string; text: string }[] | undefined;
|
||||||
|
heading?: string;
|
||||||
|
}>) {
|
||||||
|
if (!entries || entries.length === 0) return null;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SectionDivider />
|
||||||
|
{heading ? (
|
||||||
|
<h3 className="font-bold text-base text-stat-heading">{heading}</h3>
|
||||||
|
) : null}
|
||||||
|
<div className="space-y-2">
|
||||||
|
{entries.map((e) => (
|
||||||
|
<div key={e.name} className="text-sm">
|
||||||
|
<span className="font-semibold italic">{e.name}.</span> {e.text}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function StatBlock({ creature }: Readonly<StatBlockProps>) {
|
export function StatBlock({ creature }: Readonly<StatBlockProps>) {
|
||||||
const abilities = [
|
const abilities = [
|
||||||
{ label: "STR", score: creature.abilities.str },
|
{ label: "STR", score: creature.abilities.str },
|
||||||
@@ -134,19 +159,7 @@ export function StatBlock({ creature }: Readonly<StatBlockProps>) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Traits */}
|
<TraitSection entries={creature.traits} />
|
||||||
{creature.traits && creature.traits.length > 0 && (
|
|
||||||
<>
|
|
||||||
<SectionDivider />
|
|
||||||
<div className="space-y-2">
|
|
||||||
{creature.traits.map((t) => (
|
|
||||||
<div key={t.name} className="text-sm">
|
|
||||||
<span className="font-semibold italic">{t.name}.</span> {t.text}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Spellcasting */}
|
{/* Spellcasting */}
|
||||||
{creature.spellcasting && creature.spellcasting.length > 0 && (
|
{creature.spellcasting && creature.spellcasting.length > 0 && (
|
||||||
@@ -190,52 +203,9 @@ export function StatBlock({ creature }: Readonly<StatBlockProps>) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Actions */}
|
<TraitSection entries={creature.actions} heading="Actions" />
|
||||||
{creature.actions && creature.actions.length > 0 && (
|
<TraitSection entries={creature.bonusActions} heading="Bonus Actions" />
|
||||||
<>
|
<TraitSection entries={creature.reactions} heading="Reactions" />
|
||||||
<SectionDivider />
|
|
||||||
<h3 className="font-bold text-base text-stat-heading">Actions</h3>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{creature.actions.map((a) => (
|
|
||||||
<div key={a.name} className="text-sm">
|
|
||||||
<span className="font-semibold italic">{a.name}.</span> {a.text}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Bonus Actions */}
|
|
||||||
{creature.bonusActions && creature.bonusActions.length > 0 && (
|
|
||||||
<>
|
|
||||||
<SectionDivider />
|
|
||||||
<h3 className="font-bold text-base text-stat-heading">
|
|
||||||
Bonus Actions
|
|
||||||
</h3>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{creature.bonusActions.map((a) => (
|
|
||||||
<div key={a.name} className="text-sm">
|
|
||||||
<span className="font-semibold italic">{a.name}.</span> {a.text}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Reactions */}
|
|
||||||
{creature.reactions && creature.reactions.length > 0 && (
|
|
||||||
<>
|
|
||||||
<SectionDivider />
|
|
||||||
<h3 className="font-bold text-base text-stat-heading">Reactions</h3>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{creature.reactions.map((a) => (
|
|
||||||
<div key={a.name} className="text-sm">
|
|
||||||
<span className="font-semibold italic">{a.name}.</span> {a.text}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Legendary Actions */}
|
{/* Legendary Actions */}
|
||||||
{!!creature.legendaryActions && (
|
{!!creature.legendaryActions && (
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
import { useClickOutside } from "../../hooks/use-click-outside.js";
|
||||||
import { cn } from "../../lib/utils";
|
import { cn } from "../../lib/utils";
|
||||||
import { Button } from "./button";
|
import { Button } from "./button";
|
||||||
|
|
||||||
@@ -42,32 +43,7 @@ export function ConfirmButton({
|
|||||||
return () => clearTimeout(timerRef.current);
|
return () => clearTimeout(timerRef.current);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Click-outside listener when confirming
|
useClickOutside(wrapperRef, revert, isConfirming);
|
||||||
useEffect(() => {
|
|
||||||
if (!isConfirming) return;
|
|
||||||
|
|
||||||
function handleMouseDown(e: MouseEvent) {
|
|
||||||
if (
|
|
||||||
wrapperRef.current &&
|
|
||||||
!wrapperRef.current.contains(e.target as Node)
|
|
||||||
) {
|
|
||||||
revert();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleEscapeKey(e: KeyboardEvent) {
|
|
||||||
if (e.key === "Escape") {
|
|
||||||
revert();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener("mousedown", handleMouseDown);
|
|
||||||
document.addEventListener("keydown", handleEscapeKey);
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener("mousedown", handleMouseDown);
|
|
||||||
document.removeEventListener("keydown", handleEscapeKey);
|
|
||||||
};
|
|
||||||
}, [isConfirming, revert]);
|
|
||||||
|
|
||||||
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import { X } from "lucide-react";
|
||||||
import { type ReactNode, useEffect, useRef } from "react";
|
import { type ReactNode, useEffect, useRef } from "react";
|
||||||
import { cn } from "../../lib/utils.js";
|
import { cn } from "../../lib/utils.js";
|
||||||
|
import { Button } from "./button.js";
|
||||||
|
|
||||||
interface DialogProps {
|
interface DialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -48,3 +50,22 @@ export function Dialog({ open, onClose, className, children }: DialogProps) {
|
|||||||
</dialog>
|
</dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function DialogHeader({
|
||||||
|
title,
|
||||||
|
onClose,
|
||||||
|
}: Readonly<{ title: string; onClose: () => void }>) {
|
||||||
|
return (
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<h2 className="font-semibold text-foreground text-lg">{title}</h2>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon-sm"
|
||||||
|
onClick={onClose}
|
||||||
|
className="text-muted-foreground"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { EllipsisVertical } from "lucide-react";
|
import { EllipsisVertical } from "lucide-react";
|
||||||
import { type ReactNode, useEffect, useRef, useState } from "react";
|
import { type ReactNode, useRef, useState } from "react";
|
||||||
|
import { useClickOutside } from "../../hooks/use-click-outside.js";
|
||||||
import { Button } from "./button";
|
import { Button } from "./button";
|
||||||
|
|
||||||
export interface OverflowMenuItem {
|
export interface OverflowMenuItem {
|
||||||
@@ -18,23 +19,7 @@ export function OverflowMenu({ items }: OverflowMenuProps) {
|
|||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useClickOutside(ref, () => setOpen(false), open);
|
||||||
if (!open) return;
|
|
||||||
function handleMouseDown(e: MouseEvent) {
|
|
||||||
if (ref.current && !ref.current.contains(e.target as Node)) {
|
|
||||||
setOpen(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function handleKeyDown(e: KeyboardEvent) {
|
|
||||||
if (e.key === "Escape") setOpen(false);
|
|
||||||
}
|
|
||||||
document.addEventListener("mousedown", handleMouseDown);
|
|
||||||
document.addEventListener("keydown", handleKeyDown);
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener("mousedown", handleMouseDown);
|
|
||||||
document.removeEventListener("keydown", handleKeyDown);
|
|
||||||
};
|
|
||||||
}, [open]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} className="relative">
|
<div ref={ref} className="relative">
|
||||||
|
|||||||
27
apps/web/src/hooks/use-click-outside.ts
Normal file
27
apps/web/src/hooks/use-click-outside.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import type { RefObject } from "react";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
export function useClickOutside(
|
||||||
|
ref: RefObject<HTMLElement | null>,
|
||||||
|
onClose: () => void,
|
||||||
|
active = true,
|
||||||
|
): void {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!active) return;
|
||||||
|
|
||||||
|
function handleMouseDown(e: MouseEvent) {
|
||||||
|
if (ref.current && !ref.current.contains(e.target as Node)) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function handleKeyDown(e: KeyboardEvent) {
|
||||||
|
if (e.key === "Escape") onClose();
|
||||||
|
}
|
||||||
|
document.addEventListener("mousedown", handleMouseDown);
|
||||||
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("mousedown", handleMouseDown);
|
||||||
|
document.removeEventListener("keydown", handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [ref, onClose, active]);
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ import type {
|
|||||||
CombatantInit,
|
CombatantInit,
|
||||||
ConditionId,
|
ConditionId,
|
||||||
CreatureId,
|
CreatureId,
|
||||||
|
DomainError,
|
||||||
DomainEvent,
|
DomainEvent,
|
||||||
Encounter,
|
Encounter,
|
||||||
PlayerCharacter,
|
PlayerCharacter,
|
||||||
@@ -120,167 +121,90 @@ export function useEncounter() {
|
|||||||
return result;
|
return result;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const advanceTurn = useCallback(() => {
|
const dispatchAction = useCallback(
|
||||||
const result = withUndo(() => advanceTurnUseCase(makeStore()));
|
(action: () => DomainEvent[] | DomainError) => {
|
||||||
|
const result = withUndo(action);
|
||||||
if (isDomainError(result)) {
|
if (!isDomainError(result)) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setEvents((prev) => [...prev, ...result]);
|
setEvents((prev) => [...prev, ...result]);
|
||||||
}, [makeStore, withUndo]);
|
|
||||||
|
|
||||||
const retreatTurn = useCallback(() => {
|
|
||||||
const result = withUndo(() => retreatTurnUseCase(makeStore()));
|
|
||||||
|
|
||||||
if (isDomainError(result)) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
setEvents((prev) => [...prev, ...result]);
|
[withUndo],
|
||||||
}, [makeStore, withUndo]);
|
);
|
||||||
|
|
||||||
const nextId = useRef(deriveNextId(encounter));
|
const nextId = useRef(deriveNextId(encounter));
|
||||||
|
|
||||||
|
const advanceTurn = useCallback(
|
||||||
|
() => dispatchAction(() => advanceTurnUseCase(makeStore())),
|
||||||
|
[makeStore, dispatchAction],
|
||||||
|
);
|
||||||
|
|
||||||
|
const retreatTurn = useCallback(
|
||||||
|
() => dispatchAction(() => retreatTurnUseCase(makeStore())),
|
||||||
|
[makeStore, dispatchAction],
|
||||||
|
);
|
||||||
|
|
||||||
const addCombatant = useCallback(
|
const addCombatant = useCallback(
|
||||||
(name: string, init?: CombatantInit) => {
|
(name: string, init?: CombatantInit) => {
|
||||||
const id = combatantId(`c-${++nextId.current}`);
|
const id = combatantId(`c-${++nextId.current}`);
|
||||||
const result = withUndo(() =>
|
dispatchAction(() => addCombatantUseCase(makeStore(), id, name, init));
|
||||||
addCombatantUseCase(makeStore(), id, name, init),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isDomainError(result)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setEvents((prev) => [...prev, ...result]);
|
|
||||||
},
|
},
|
||||||
[makeStore, withUndo],
|
[makeStore, dispatchAction],
|
||||||
);
|
);
|
||||||
|
|
||||||
const removeCombatant = useCallback(
|
const removeCombatant = useCallback(
|
||||||
(id: CombatantId) => {
|
(id: CombatantId) =>
|
||||||
const result = withUndo(() => removeCombatantUseCase(makeStore(), id));
|
dispatchAction(() => removeCombatantUseCase(makeStore(), id)),
|
||||||
|
[makeStore, dispatchAction],
|
||||||
if (isDomainError(result)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setEvents((prev) => [...prev, ...result]);
|
|
||||||
},
|
|
||||||
[makeStore, withUndo],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const editCombatant = useCallback(
|
const editCombatant = useCallback(
|
||||||
(id: CombatantId, newName: string) => {
|
(id: CombatantId, newName: string) =>
|
||||||
const result = withUndo(() =>
|
dispatchAction(() => editCombatantUseCase(makeStore(), id, newName)),
|
||||||
editCombatantUseCase(makeStore(), id, newName),
|
[makeStore, dispatchAction],
|
||||||
);
|
|
||||||
|
|
||||||
if (isDomainError(result)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setEvents((prev) => [...prev, ...result]);
|
|
||||||
},
|
|
||||||
[makeStore, withUndo],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const setInitiative = useCallback(
|
const setInitiative = useCallback(
|
||||||
(id: CombatantId, value: number | undefined) => {
|
(id: CombatantId, value: number | undefined) =>
|
||||||
const result = withUndo(() =>
|
dispatchAction(() => setInitiativeUseCase(makeStore(), id, value)),
|
||||||
setInitiativeUseCase(makeStore(), id, value),
|
[makeStore, dispatchAction],
|
||||||
);
|
|
||||||
|
|
||||||
if (isDomainError(result)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setEvents((prev) => [...prev, ...result]);
|
|
||||||
},
|
|
||||||
[makeStore, withUndo],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const setHp = useCallback(
|
const setHp = useCallback(
|
||||||
(id: CombatantId, maxHp: number | undefined) => {
|
(id: CombatantId, maxHp: number | undefined) =>
|
||||||
const result = withUndo(() => setHpUseCase(makeStore(), id, maxHp));
|
dispatchAction(() => setHpUseCase(makeStore(), id, maxHp)),
|
||||||
|
[makeStore, dispatchAction],
|
||||||
if (isDomainError(result)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setEvents((prev) => [...prev, ...result]);
|
|
||||||
},
|
|
||||||
[makeStore, withUndo],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const adjustHp = useCallback(
|
const adjustHp = useCallback(
|
||||||
(id: CombatantId, delta: number) => {
|
(id: CombatantId, delta: number) =>
|
||||||
const result = withUndo(() => adjustHpUseCase(makeStore(), id, delta));
|
dispatchAction(() => adjustHpUseCase(makeStore(), id, delta)),
|
||||||
|
[makeStore, dispatchAction],
|
||||||
if (isDomainError(result)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setEvents((prev) => [...prev, ...result]);
|
|
||||||
},
|
|
||||||
[makeStore, withUndo],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const setTempHp = useCallback(
|
const setTempHp = useCallback(
|
||||||
(id: CombatantId, tempHp: number | undefined) => {
|
(id: CombatantId, tempHp: number | undefined) =>
|
||||||
const result = withUndo(() => setTempHpUseCase(makeStore(), id, tempHp));
|
dispatchAction(() => setTempHpUseCase(makeStore(), id, tempHp)),
|
||||||
|
[makeStore, dispatchAction],
|
||||||
if (isDomainError(result)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setEvents((prev) => [...prev, ...result]);
|
|
||||||
},
|
|
||||||
[makeStore, withUndo],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const setAc = useCallback(
|
const setAc = useCallback(
|
||||||
(id: CombatantId, value: number | undefined) => {
|
(id: CombatantId, value: number | undefined) =>
|
||||||
const result = withUndo(() => setAcUseCase(makeStore(), id, value));
|
dispatchAction(() => setAcUseCase(makeStore(), id, value)),
|
||||||
|
[makeStore, dispatchAction],
|
||||||
if (isDomainError(result)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setEvents((prev) => [...prev, ...result]);
|
|
||||||
},
|
|
||||||
[makeStore, withUndo],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const toggleCondition = useCallback(
|
const toggleCondition = useCallback(
|
||||||
(id: CombatantId, conditionId: ConditionId) => {
|
(id: CombatantId, conditionId: ConditionId) =>
|
||||||
const result = withUndo(() =>
|
dispatchAction(() =>
|
||||||
toggleConditionUseCase(makeStore(), id, conditionId),
|
toggleConditionUseCase(makeStore(), id, conditionId),
|
||||||
);
|
),
|
||||||
|
[makeStore, dispatchAction],
|
||||||
if (isDomainError(result)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setEvents((prev) => [...prev, ...result]);
|
|
||||||
},
|
|
||||||
[makeStore, withUndo],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const toggleConcentration = useCallback(
|
const toggleConcentration = useCallback(
|
||||||
(id: CombatantId) => {
|
(id: CombatantId) =>
|
||||||
const result = withUndo(() =>
|
dispatchAction(() => toggleConcentrationUseCase(makeStore(), id)),
|
||||||
toggleConcentrationUseCase(makeStore(), id),
|
[makeStore, dispatchAction],
|
||||||
);
|
|
||||||
|
|
||||||
if (isDomainError(result)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setEvents((prev) => [...prev, ...result]);
|
|
||||||
},
|
|
||||||
[makeStore, withUndo],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const clearEncounter = useCallback(() => {
|
const clearEncounter = useCallback(() => {
|
||||||
@@ -298,16 +222,11 @@ export function useEncounter() {
|
|||||||
setEvents((prev) => [...prev, ...result]);
|
setEvents((prev) => [...prev, ...result]);
|
||||||
}, [makeStore]);
|
}, [makeStore]);
|
||||||
|
|
||||||
const addOneFromBestiary = useCallback(
|
const resolveAndRename = useCallback(
|
||||||
(
|
(name: string): string => {
|
||||||
entry: BestiaryIndexEntry,
|
|
||||||
): { cId: CreatureId; events: DomainEvent[] } | null => {
|
|
||||||
const store = makeStore();
|
const store = makeStore();
|
||||||
const existingNames = store.get().combatants.map((c) => c.name);
|
const existingNames = store.get().combatants.map((c) => c.name);
|
||||||
const { newName, renames } = resolveCreatureName(
|
const { newName, renames } = resolveCreatureName(name, existingNames);
|
||||||
entry.name,
|
|
||||||
existingNames,
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const { from, to } of renames) {
|
for (const { from, to } of renames) {
|
||||||
const target = store.get().combatants.find((c) => c.name === from);
|
const target = store.get().combatants.find((c) => c.name === from);
|
||||||
@@ -316,6 +235,17 @@ export function useEncounter() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return newName;
|
||||||
|
},
|
||||||
|
[makeStore],
|
||||||
|
);
|
||||||
|
|
||||||
|
const addOneFromBestiary = useCallback(
|
||||||
|
(
|
||||||
|
entry: BestiaryIndexEntry,
|
||||||
|
): { cId: CreatureId; events: DomainEvent[] } | null => {
|
||||||
|
const newName = resolveAndRename(entry.name);
|
||||||
|
|
||||||
const slug = entry.name
|
const slug = entry.name
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replaceAll(/[^a-z0-9]+/g, "-")
|
.replaceAll(/[^a-z0-9]+/g, "-")
|
||||||
@@ -333,7 +263,7 @@ export function useEncounter() {
|
|||||||
|
|
||||||
return { cId, events: result };
|
return { cId, events: result };
|
||||||
},
|
},
|
||||||
[makeStore],
|
[makeStore, resolveAndRename],
|
||||||
);
|
);
|
||||||
|
|
||||||
const addFromBestiary = useCallback(
|
const addFromBestiary = useCallback(
|
||||||
@@ -385,16 +315,7 @@ export function useEncounter() {
|
|||||||
const addFromPlayerCharacter = useCallback(
|
const addFromPlayerCharacter = useCallback(
|
||||||
(pc: PlayerCharacter) => {
|
(pc: PlayerCharacter) => {
|
||||||
const snapshot = encounterRef.current;
|
const snapshot = encounterRef.current;
|
||||||
const store = makeStore();
|
const newName = resolveAndRename(pc.name);
|
||||||
const existingNames = store.get().combatants.map((c) => c.name);
|
|
||||||
const { newName, renames } = resolveCreatureName(pc.name, existingNames);
|
|
||||||
|
|
||||||
for (const { from, to } of renames) {
|
|
||||||
const target = store.get().combatants.find((c) => c.name === from);
|
|
||||||
if (target) {
|
|
||||||
editCombatantUseCase(makeStore(), target.id, to);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const id = combatantId(`c-${++nextId.current}`);
|
const id = combatantId(`c-${++nextId.current}`);
|
||||||
const result = addCombatantUseCase(makeStore(), id, newName, {
|
const result = addCombatantUseCase(makeStore(), id, newName, {
|
||||||
@@ -406,7 +327,7 @@ export function useEncounter() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (isDomainError(result)) {
|
if (isDomainError(result)) {
|
||||||
store.save(snapshot);
|
makeStore().save(snapshot);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -416,7 +337,7 @@ export function useEncounter() {
|
|||||||
|
|
||||||
setEvents((prev) => [...prev, ...result]);
|
setEvents((prev) => [...prev, ...result]);
|
||||||
},
|
},
|
||||||
[makeStore],
|
[makeStore, resolveAndRename],
|
||||||
);
|
);
|
||||||
|
|
||||||
const undoAction = useCallback(() => {
|
const undoAction = useCallback(() => {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://unpkg.com/knip@5/schema.json",
|
"$schema": "https://unpkg.com/knip@5/schema.json",
|
||||||
|
"ignoreDependencies": ["jsinspect-plus"],
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
".": {
|
".": {
|
||||||
"entry": ["scripts/*.mjs"]
|
"entry": ["scripts/*.mjs"]
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"@biomejs/biome": "2.4.8",
|
"@biomejs/biome": "2.4.8",
|
||||||
"@vitest/coverage-v8": "^4.1.0",
|
"@vitest/coverage-v8": "^4.1.0",
|
||||||
"jscpd": "^4.0.8",
|
"jscpd": "^4.0.8",
|
||||||
|
"jsinspect-plus": "^3.1.3",
|
||||||
"knip": "^5.88.1",
|
"knip": "^5.88.1",
|
||||||
"lefthook": "^2.1.4",
|
"lefthook": "^2.1.4",
|
||||||
"oxlint": "^1.56.0",
|
"oxlint": "^1.56.0",
|
||||||
@@ -29,10 +30,11 @@
|
|||||||
"test:watch": "vitest",
|
"test:watch": "vitest",
|
||||||
"knip": "knip",
|
"knip": "knip",
|
||||||
"jscpd": "jscpd",
|
"jscpd": "jscpd",
|
||||||
|
"jsinspect": "jsinspect -c .jsinspectrc apps/web/src packages/domain/src packages/application/src",
|
||||||
"oxlint": "oxlint --tsconfig apps/web/tsconfig.json --type-aware",
|
"oxlint": "oxlint --tsconfig apps/web/tsconfig.json --type-aware",
|
||||||
"check:ignores": "node scripts/check-lint-ignores.mjs",
|
"check:ignores": "node scripts/check-lint-ignores.mjs",
|
||||||
"check:classnames": "node scripts/check-cn-classnames.mjs",
|
"check:classnames": "node scripts/check-cn-classnames.mjs",
|
||||||
"check:props": "node scripts/check-component-props.mjs",
|
"check:props": "node scripts/check-component-props.mjs",
|
||||||
"check": "pnpm audit --audit-level=high && knip && biome check . && oxlint --tsconfig apps/web/tsconfig.json --type-aware && node scripts/check-lint-ignores.mjs && node scripts/check-cn-classnames.mjs && node scripts/check-component-props.mjs && tsc --build && vitest run && jscpd"
|
"check": "pnpm audit --audit-level=high && knip && biome check . && oxlint --tsconfig apps/web/tsconfig.json --type-aware && node scripts/check-lint-ignores.mjs && node scripts/check-cn-classnames.mjs && node scripts/check-component-props.mjs && tsc --build && vitest run && jscpd && pnpm jsinspect"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import {
|
|||||||
type CombatantInit,
|
type CombatantInit,
|
||||||
type DomainError,
|
type DomainError,
|
||||||
type DomainEvent,
|
type DomainEvent,
|
||||||
isDomainError,
|
|
||||||
} from "@initiative/domain";
|
} from "@initiative/domain";
|
||||||
import type { EncounterStore } from "./ports.js";
|
import type { EncounterStore } from "./ports.js";
|
||||||
|
import { runEncounterAction } from "./run-encounter-action.js";
|
||||||
|
|
||||||
export function addCombatantUseCase(
|
export function addCombatantUseCase(
|
||||||
store: EncounterStore,
|
store: EncounterStore,
|
||||||
@@ -14,13 +14,7 @@ export function addCombatantUseCase(
|
|||||||
name: string,
|
name: string,
|
||||||
init?: CombatantInit,
|
init?: CombatantInit,
|
||||||
): DomainEvent[] | DomainError {
|
): DomainEvent[] | DomainError {
|
||||||
const encounter = store.get();
|
return runEncounterAction(store, (encounter) =>
|
||||||
const result = addCombatant(encounter, id, name, init);
|
addCombatant(encounter, id, name, init),
|
||||||
|
);
|
||||||
if (isDomainError(result)) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
store.save(result.encounter);
|
|
||||||
return result.events;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,22 +3,16 @@ import {
|
|||||||
type CombatantId,
|
type CombatantId,
|
||||||
type DomainError,
|
type DomainError,
|
||||||
type DomainEvent,
|
type DomainEvent,
|
||||||
isDomainError,
|
|
||||||
} from "@initiative/domain";
|
} from "@initiative/domain";
|
||||||
import type { EncounterStore } from "./ports.js";
|
import type { EncounterStore } from "./ports.js";
|
||||||
|
import { runEncounterAction } from "./run-encounter-action.js";
|
||||||
|
|
||||||
export function adjustHpUseCase(
|
export function adjustHpUseCase(
|
||||||
store: EncounterStore,
|
store: EncounterStore,
|
||||||
combatantId: CombatantId,
|
combatantId: CombatantId,
|
||||||
delta: number,
|
delta: number,
|
||||||
): DomainEvent[] | DomainError {
|
): DomainEvent[] | DomainError {
|
||||||
const encounter = store.get();
|
return runEncounterAction(store, (encounter) =>
|
||||||
const result = adjustHp(encounter, combatantId, delta);
|
adjustHp(encounter, combatantId, delta),
|
||||||
|
);
|
||||||
if (isDomainError(result)) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
store.save(result.encounter);
|
|
||||||
return result.events;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,20 +2,12 @@ import {
|
|||||||
advanceTurn,
|
advanceTurn,
|
||||||
type DomainError,
|
type DomainError,
|
||||||
type DomainEvent,
|
type DomainEvent,
|
||||||
isDomainError,
|
|
||||||
} from "@initiative/domain";
|
} from "@initiative/domain";
|
||||||
import type { EncounterStore } from "./ports.js";
|
import type { EncounterStore } from "./ports.js";
|
||||||
|
import { runEncounterAction } from "./run-encounter-action.js";
|
||||||
|
|
||||||
export function advanceTurnUseCase(
|
export function advanceTurnUseCase(
|
||||||
store: EncounterStore,
|
store: EncounterStore,
|
||||||
): DomainEvent[] | DomainError {
|
): DomainEvent[] | DomainError {
|
||||||
const encounter = store.get();
|
return runEncounterAction(store, (encounter) => advanceTurn(encounter));
|
||||||
const result = advanceTurn(encounter);
|
|
||||||
|
|
||||||
if (isDomainError(result)) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
store.save(result.encounter);
|
|
||||||
return result.events;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,20 +2,12 @@ import {
|
|||||||
clearEncounter,
|
clearEncounter,
|
||||||
type DomainError,
|
type DomainError,
|
||||||
type DomainEvent,
|
type DomainEvent,
|
||||||
isDomainError,
|
|
||||||
} from "@initiative/domain";
|
} from "@initiative/domain";
|
||||||
import type { EncounterStore } from "./ports.js";
|
import type { EncounterStore } from "./ports.js";
|
||||||
|
import { runEncounterAction } from "./run-encounter-action.js";
|
||||||
|
|
||||||
export function clearEncounterUseCase(
|
export function clearEncounterUseCase(
|
||||||
store: EncounterStore,
|
store: EncounterStore,
|
||||||
): DomainEvent[] | DomainError {
|
): DomainEvent[] | DomainError {
|
||||||
const encounter = store.get();
|
return runEncounterAction(store, (encounter) => clearEncounter(encounter));
|
||||||
const result = clearEncounter(encounter);
|
|
||||||
|
|
||||||
if (isDomainError(result)) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
store.save(result.encounter);
|
|
||||||
return result.events;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,22 +3,16 @@ import {
|
|||||||
type DomainError,
|
type DomainError,
|
||||||
type DomainEvent,
|
type DomainEvent,
|
||||||
editCombatant,
|
editCombatant,
|
||||||
isDomainError,
|
|
||||||
} from "@initiative/domain";
|
} from "@initiative/domain";
|
||||||
import type { EncounterStore } from "./ports.js";
|
import type { EncounterStore } from "./ports.js";
|
||||||
|
import { runEncounterAction } from "./run-encounter-action.js";
|
||||||
|
|
||||||
export function editCombatantUseCase(
|
export function editCombatantUseCase(
|
||||||
store: EncounterStore,
|
store: EncounterStore,
|
||||||
id: CombatantId,
|
id: CombatantId,
|
||||||
newName: string,
|
newName: string,
|
||||||
): DomainEvent[] | DomainError {
|
): DomainEvent[] | DomainError {
|
||||||
const encounter = store.get();
|
return runEncounterAction(store, (encounter) =>
|
||||||
const result = editCombatant(encounter, id, newName);
|
editCombatant(encounter, id, newName),
|
||||||
|
);
|
||||||
if (isDomainError(result)) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
store.save(result.encounter);
|
|
||||||
return result.events;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,22 +2,16 @@ import {
|
|||||||
type CombatantId,
|
type CombatantId,
|
||||||
type DomainError,
|
type DomainError,
|
||||||
type DomainEvent,
|
type DomainEvent,
|
||||||
isDomainError,
|
|
||||||
removeCombatant,
|
removeCombatant,
|
||||||
} from "@initiative/domain";
|
} from "@initiative/domain";
|
||||||
import type { EncounterStore } from "./ports.js";
|
import type { EncounterStore } from "./ports.js";
|
||||||
|
import { runEncounterAction } from "./run-encounter-action.js";
|
||||||
|
|
||||||
export function removeCombatantUseCase(
|
export function removeCombatantUseCase(
|
||||||
store: EncounterStore,
|
store: EncounterStore,
|
||||||
id: CombatantId,
|
id: CombatantId,
|
||||||
): DomainEvent[] | DomainError {
|
): DomainEvent[] | DomainError {
|
||||||
const encounter = store.get();
|
return runEncounterAction(store, (encounter) =>
|
||||||
const result = removeCombatant(encounter, id);
|
removeCombatant(encounter, id),
|
||||||
|
);
|
||||||
if (isDomainError(result)) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
store.save(result.encounter);
|
|
||||||
return result.events;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
type DomainError,
|
type DomainError,
|
||||||
type DomainEvent,
|
type DomainEvent,
|
||||||
isDomainError,
|
|
||||||
retreatTurn,
|
retreatTurn,
|
||||||
} from "@initiative/domain";
|
} from "@initiative/domain";
|
||||||
import type { EncounterStore } from "./ports.js";
|
import type { EncounterStore } from "./ports.js";
|
||||||
|
import { runEncounterAction } from "./run-encounter-action.js";
|
||||||
|
|
||||||
export function retreatTurnUseCase(
|
export function retreatTurnUseCase(
|
||||||
store: EncounterStore,
|
store: EncounterStore,
|
||||||
): DomainEvent[] | DomainError {
|
): DomainEvent[] | DomainError {
|
||||||
const encounter = store.get();
|
return runEncounterAction(store, (encounter) => retreatTurn(encounter));
|
||||||
const result = retreatTurn(encounter);
|
|
||||||
|
|
||||||
if (isDomainError(result)) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
store.save(result.encounter);
|
|
||||||
return result.events;
|
|
||||||
}
|
}
|
||||||
|
|||||||
27
packages/application/src/run-encounter-action.ts
Normal file
27
packages/application/src/run-encounter-action.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import {
|
||||||
|
type DomainError,
|
||||||
|
type DomainEvent,
|
||||||
|
type Encounter,
|
||||||
|
isDomainError,
|
||||||
|
} from "@initiative/domain";
|
||||||
|
import type { EncounterStore } from "./ports.js";
|
||||||
|
|
||||||
|
interface EncounterActionResult {
|
||||||
|
readonly encounter: Encounter;
|
||||||
|
readonly events: DomainEvent[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runEncounterAction(
|
||||||
|
store: EncounterStore,
|
||||||
|
action: (encounter: Encounter) => EncounterActionResult | DomainError,
|
||||||
|
): DomainEvent[] | DomainError {
|
||||||
|
const encounter = store.get();
|
||||||
|
const result = action(encounter);
|
||||||
|
|
||||||
|
if (isDomainError(result)) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
store.save(result.encounter);
|
||||||
|
return result.events;
|
||||||
|
}
|
||||||
@@ -2,23 +2,17 @@ import {
|
|||||||
type CombatantId,
|
type CombatantId,
|
||||||
type DomainError,
|
type DomainError,
|
||||||
type DomainEvent,
|
type DomainEvent,
|
||||||
isDomainError,
|
|
||||||
setAc,
|
setAc,
|
||||||
} from "@initiative/domain";
|
} from "@initiative/domain";
|
||||||
import type { EncounterStore } from "./ports.js";
|
import type { EncounterStore } from "./ports.js";
|
||||||
|
import { runEncounterAction } from "./run-encounter-action.js";
|
||||||
|
|
||||||
export function setAcUseCase(
|
export function setAcUseCase(
|
||||||
store: EncounterStore,
|
store: EncounterStore,
|
||||||
combatantId: CombatantId,
|
combatantId: CombatantId,
|
||||||
value: number | undefined,
|
value: number | undefined,
|
||||||
): DomainEvent[] | DomainError {
|
): DomainEvent[] | DomainError {
|
||||||
const encounter = store.get();
|
return runEncounterAction(store, (encounter) =>
|
||||||
const result = setAc(encounter, combatantId, value);
|
setAc(encounter, combatantId, value),
|
||||||
|
);
|
||||||
if (isDomainError(result)) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
store.save(result.encounter);
|
|
||||||
return result.events;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,23 +2,17 @@ import {
|
|||||||
type CombatantId,
|
type CombatantId,
|
||||||
type DomainError,
|
type DomainError,
|
||||||
type DomainEvent,
|
type DomainEvent,
|
||||||
isDomainError,
|
|
||||||
setHp,
|
setHp,
|
||||||
} from "@initiative/domain";
|
} from "@initiative/domain";
|
||||||
import type { EncounterStore } from "./ports.js";
|
import type { EncounterStore } from "./ports.js";
|
||||||
|
import { runEncounterAction } from "./run-encounter-action.js";
|
||||||
|
|
||||||
export function setHpUseCase(
|
export function setHpUseCase(
|
||||||
store: EncounterStore,
|
store: EncounterStore,
|
||||||
combatantId: CombatantId,
|
combatantId: CombatantId,
|
||||||
maxHp: number | undefined,
|
maxHp: number | undefined,
|
||||||
): DomainEvent[] | DomainError {
|
): DomainEvent[] | DomainError {
|
||||||
const encounter = store.get();
|
return runEncounterAction(store, (encounter) =>
|
||||||
const result = setHp(encounter, combatantId, maxHp);
|
setHp(encounter, combatantId, maxHp),
|
||||||
|
);
|
||||||
if (isDomainError(result)) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
store.save(result.encounter);
|
|
||||||
return result.events;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,23 +2,17 @@ import {
|
|||||||
type CombatantId,
|
type CombatantId,
|
||||||
type DomainError,
|
type DomainError,
|
||||||
type DomainEvent,
|
type DomainEvent,
|
||||||
isDomainError,
|
|
||||||
setInitiative,
|
setInitiative,
|
||||||
} from "@initiative/domain";
|
} from "@initiative/domain";
|
||||||
import type { EncounterStore } from "./ports.js";
|
import type { EncounterStore } from "./ports.js";
|
||||||
|
import { runEncounterAction } from "./run-encounter-action.js";
|
||||||
|
|
||||||
export function setInitiativeUseCase(
|
export function setInitiativeUseCase(
|
||||||
store: EncounterStore,
|
store: EncounterStore,
|
||||||
combatantId: CombatantId,
|
combatantId: CombatantId,
|
||||||
value: number | undefined,
|
value: number | undefined,
|
||||||
): DomainEvent[] | DomainError {
|
): DomainEvent[] | DomainError {
|
||||||
const encounter = store.get();
|
return runEncounterAction(store, (encounter) =>
|
||||||
const result = setInitiative(encounter, combatantId, value);
|
setInitiative(encounter, combatantId, value),
|
||||||
|
);
|
||||||
if (isDomainError(result)) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
store.save(result.encounter);
|
|
||||||
return result.events;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,23 +2,17 @@ import {
|
|||||||
type CombatantId,
|
type CombatantId,
|
||||||
type DomainError,
|
type DomainError,
|
||||||
type DomainEvent,
|
type DomainEvent,
|
||||||
isDomainError,
|
|
||||||
setTempHp,
|
setTempHp,
|
||||||
} from "@initiative/domain";
|
} from "@initiative/domain";
|
||||||
import type { EncounterStore } from "./ports.js";
|
import type { EncounterStore } from "./ports.js";
|
||||||
|
import { runEncounterAction } from "./run-encounter-action.js";
|
||||||
|
|
||||||
export function setTempHpUseCase(
|
export function setTempHpUseCase(
|
||||||
store: EncounterStore,
|
store: EncounterStore,
|
||||||
combatantId: CombatantId,
|
combatantId: CombatantId,
|
||||||
tempHp: number | undefined,
|
tempHp: number | undefined,
|
||||||
): DomainEvent[] | DomainError {
|
): DomainEvent[] | DomainError {
|
||||||
const encounter = store.get();
|
return runEncounterAction(store, (encounter) =>
|
||||||
const result = setTempHp(encounter, combatantId, tempHp);
|
setTempHp(encounter, combatantId, tempHp),
|
||||||
|
);
|
||||||
if (isDomainError(result)) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
store.save(result.encounter);
|
|
||||||
return result.events;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,22 +2,16 @@ import {
|
|||||||
type CombatantId,
|
type CombatantId,
|
||||||
type DomainError,
|
type DomainError,
|
||||||
type DomainEvent,
|
type DomainEvent,
|
||||||
isDomainError,
|
|
||||||
toggleConcentration,
|
toggleConcentration,
|
||||||
} from "@initiative/domain";
|
} from "@initiative/domain";
|
||||||
import type { EncounterStore } from "./ports.js";
|
import type { EncounterStore } from "./ports.js";
|
||||||
|
import { runEncounterAction } from "./run-encounter-action.js";
|
||||||
|
|
||||||
export function toggleConcentrationUseCase(
|
export function toggleConcentrationUseCase(
|
||||||
store: EncounterStore,
|
store: EncounterStore,
|
||||||
combatantId: CombatantId,
|
combatantId: CombatantId,
|
||||||
): DomainEvent[] | DomainError {
|
): DomainEvent[] | DomainError {
|
||||||
const encounter = store.get();
|
return runEncounterAction(store, (encounter) =>
|
||||||
const result = toggleConcentration(encounter, combatantId);
|
toggleConcentration(encounter, combatantId),
|
||||||
|
);
|
||||||
if (isDomainError(result)) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
store.save(result.encounter);
|
|
||||||
return result.events;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,23 +3,17 @@ import {
|
|||||||
type ConditionId,
|
type ConditionId,
|
||||||
type DomainError,
|
type DomainError,
|
||||||
type DomainEvent,
|
type DomainEvent,
|
||||||
isDomainError,
|
|
||||||
toggleCondition,
|
toggleCondition,
|
||||||
} from "@initiative/domain";
|
} from "@initiative/domain";
|
||||||
import type { EncounterStore } from "./ports.js";
|
import type { EncounterStore } from "./ports.js";
|
||||||
|
import { runEncounterAction } from "./run-encounter-action.js";
|
||||||
|
|
||||||
export function toggleConditionUseCase(
|
export function toggleConditionUseCase(
|
||||||
store: EncounterStore,
|
store: EncounterStore,
|
||||||
combatantId: CombatantId,
|
combatantId: CombatantId,
|
||||||
conditionId: ConditionId,
|
conditionId: ConditionId,
|
||||||
): DomainEvent[] | DomainError {
|
): DomainEvent[] | DomainError {
|
||||||
const encounter = store.get();
|
return runEncounterAction(store, (encounter) =>
|
||||||
const result = toggleCondition(encounter, combatantId, conditionId);
|
toggleCondition(encounter, combatantId, conditionId),
|
||||||
|
);
|
||||||
if (isDomainError(result)) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
store.save(result.encounter);
|
|
||||||
return result.events;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import type { DomainEvent } from "./events.js";
|
import type { DomainEvent } from "./events.js";
|
||||||
import type { CombatantId, DomainError, Encounter } from "./types.js";
|
import {
|
||||||
|
type CombatantId,
|
||||||
|
type DomainError,
|
||||||
|
type Encounter,
|
||||||
|
findCombatant,
|
||||||
|
isDomainError,
|
||||||
|
} from "./types.js";
|
||||||
|
|
||||||
export interface AdjustHpSuccess {
|
export interface AdjustHpSuccess {
|
||||||
readonly encounter: Encounter;
|
readonly encounter: Encounter;
|
||||||
@@ -17,17 +23,9 @@ export function adjustHp(
|
|||||||
combatantId: CombatantId,
|
combatantId: CombatantId,
|
||||||
delta: number,
|
delta: number,
|
||||||
): AdjustHpSuccess | DomainError {
|
): AdjustHpSuccess | DomainError {
|
||||||
const targetIdx = encounter.combatants.findIndex((c) => c.id === combatantId);
|
const found = findCombatant(encounter, combatantId);
|
||||||
|
if (isDomainError(found)) return found;
|
||||||
if (targetIdx === -1) {
|
const { combatant: target } = found;
|
||||||
return {
|
|
||||||
kind: "domain-error",
|
|
||||||
code: "combatant-not-found",
|
|
||||||
message: `No combatant found with ID "${combatantId}"`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const target = encounter.combatants[targetIdx];
|
|
||||||
|
|
||||||
if (target.maxHp === undefined || target.currentHp === undefined) {
|
if (target.maxHp === undefined || target.currentHp === undefined) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import type { DomainEvent } from "./events.js";
|
import type { DomainEvent } from "./events.js";
|
||||||
import type { CombatantId, DomainError, Encounter } from "./types.js";
|
import {
|
||||||
|
type CombatantId,
|
||||||
|
type DomainError,
|
||||||
|
type Encounter,
|
||||||
|
findCombatant,
|
||||||
|
isDomainError,
|
||||||
|
} from "./types.js";
|
||||||
|
|
||||||
export interface EditCombatantSuccess {
|
export interface EditCombatantSuccess {
|
||||||
readonly encounter: Encounter;
|
readonly encounter: Encounter;
|
||||||
@@ -30,17 +36,9 @@ export function editCombatant(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const index = encounter.combatants.findIndex((c) => c.id === id);
|
const found = findCombatant(encounter, id);
|
||||||
|
if (isDomainError(found)) return found;
|
||||||
if (index === -1) {
|
const oldName = found.combatant.name;
|
||||||
return {
|
|
||||||
kind: "domain-error",
|
|
||||||
code: "combatant-not-found",
|
|
||||||
message: `No combatant found with ID "${id}"`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldName = encounter.combatants[index].name;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
encounter: {
|
encounter: {
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ export {
|
|||||||
createEncounter,
|
createEncounter,
|
||||||
type DomainError,
|
type DomainError,
|
||||||
type Encounter,
|
type Encounter,
|
||||||
|
findCombatant,
|
||||||
isDomainError,
|
isDomainError,
|
||||||
} from "./types.js";
|
} from "./types.js";
|
||||||
export {
|
export {
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import type { DomainEvent } from "./events.js";
|
import type { DomainEvent } from "./events.js";
|
||||||
import type { CombatantId, DomainError, Encounter } from "./types.js";
|
import {
|
||||||
|
type CombatantId,
|
||||||
|
type DomainError,
|
||||||
|
type Encounter,
|
||||||
|
findCombatant,
|
||||||
|
isDomainError,
|
||||||
|
} from "./types.js";
|
||||||
|
|
||||||
export interface RemoveCombatantSuccess {
|
export interface RemoveCombatantSuccess {
|
||||||
readonly encounter: Encounter;
|
readonly encounter: Encounter;
|
||||||
@@ -22,17 +28,10 @@ export function removeCombatant(
|
|||||||
encounter: Encounter,
|
encounter: Encounter,
|
||||||
id: CombatantId,
|
id: CombatantId,
|
||||||
): RemoveCombatantSuccess | DomainError {
|
): RemoveCombatantSuccess | DomainError {
|
||||||
const removedIdx = encounter.combatants.findIndex((c) => c.id === id);
|
const found = findCombatant(encounter, id);
|
||||||
|
if (isDomainError(found)) return found;
|
||||||
|
|
||||||
if (removedIdx === -1) {
|
const { index: removedIdx, combatant: removed } = found;
|
||||||
return {
|
|
||||||
kind: "domain-error",
|
|
||||||
code: "combatant-not-found",
|
|
||||||
message: `No combatant found with ID "${id}"`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const removed = encounter.combatants[removedIdx];
|
|
||||||
const newCombatants = encounter.combatants.filter((_, i) => i !== removedIdx);
|
const newCombatants = encounter.combatants.filter((_, i) => i !== removedIdx);
|
||||||
|
|
||||||
let newActiveIndex: number;
|
let newActiveIndex: number;
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import type { DomainEvent } from "./events.js";
|
import type { DomainEvent } from "./events.js";
|
||||||
import type { CombatantId, DomainError, Encounter } from "./types.js";
|
import {
|
||||||
|
type CombatantId,
|
||||||
|
type DomainError,
|
||||||
|
type Encounter,
|
||||||
|
findCombatant,
|
||||||
|
isDomainError,
|
||||||
|
} from "./types.js";
|
||||||
|
|
||||||
export interface SetAcSuccess {
|
export interface SetAcSuccess {
|
||||||
readonly encounter: Encounter;
|
readonly encounter: Encounter;
|
||||||
@@ -11,15 +17,8 @@ export function setAc(
|
|||||||
combatantId: CombatantId,
|
combatantId: CombatantId,
|
||||||
value: number | undefined,
|
value: number | undefined,
|
||||||
): SetAcSuccess | DomainError {
|
): SetAcSuccess | DomainError {
|
||||||
const targetIdx = encounter.combatants.findIndex((c) => c.id === combatantId);
|
const found = findCombatant(encounter, combatantId);
|
||||||
|
if (isDomainError(found)) return found;
|
||||||
if (targetIdx === -1) {
|
|
||||||
return {
|
|
||||||
kind: "domain-error",
|
|
||||||
code: "combatant-not-found",
|
|
||||||
message: `No combatant found with ID "${combatantId}"`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value !== undefined && (!Number.isInteger(value) || value < 0)) {
|
if (value !== undefined && (!Number.isInteger(value) || value < 0)) {
|
||||||
return {
|
return {
|
||||||
@@ -29,8 +28,7 @@ export function setAc(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const target = encounter.combatants[targetIdx];
|
const previousAc = found.combatant.ac;
|
||||||
const previousAc = target.ac;
|
|
||||||
|
|
||||||
const updatedCombatants = encounter.combatants.map((c) =>
|
const updatedCombatants = encounter.combatants.map((c) =>
|
||||||
c.id === combatantId ? { ...c, ac: value } : c,
|
c.id === combatantId ? { ...c, ac: value } : c,
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import type { DomainEvent } from "./events.js";
|
import type { DomainEvent } from "./events.js";
|
||||||
import type { CombatantId, DomainError, Encounter } from "./types.js";
|
import {
|
||||||
|
type CombatantId,
|
||||||
|
type DomainError,
|
||||||
|
type Encounter,
|
||||||
|
findCombatant,
|
||||||
|
isDomainError,
|
||||||
|
} from "./types.js";
|
||||||
|
|
||||||
export interface SetHpSuccess {
|
export interface SetHpSuccess {
|
||||||
readonly encounter: Encounter;
|
readonly encounter: Encounter;
|
||||||
@@ -18,15 +24,8 @@ export function setHp(
|
|||||||
combatantId: CombatantId,
|
combatantId: CombatantId,
|
||||||
maxHp: number | undefined,
|
maxHp: number | undefined,
|
||||||
): SetHpSuccess | DomainError {
|
): SetHpSuccess | DomainError {
|
||||||
const targetIdx = encounter.combatants.findIndex((c) => c.id === combatantId);
|
const found = findCombatant(encounter, combatantId);
|
||||||
|
if (isDomainError(found)) return found;
|
||||||
if (targetIdx === -1) {
|
|
||||||
return {
|
|
||||||
kind: "domain-error",
|
|
||||||
code: "combatant-not-found",
|
|
||||||
message: `No combatant found with ID "${combatantId}"`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (maxHp !== undefined && (!Number.isInteger(maxHp) || maxHp < 1)) {
|
if (maxHp !== undefined && (!Number.isInteger(maxHp) || maxHp < 1)) {
|
||||||
return {
|
return {
|
||||||
@@ -36,9 +35,8 @@ export function setHp(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const target = encounter.combatants[targetIdx];
|
const previousMaxHp = found.combatant.maxHp;
|
||||||
const previousMaxHp = target.maxHp;
|
const previousCurrentHp = found.combatant.currentHp;
|
||||||
const previousCurrentHp = target.currentHp;
|
|
||||||
|
|
||||||
let newMaxHp: number | undefined;
|
let newMaxHp: number | undefined;
|
||||||
let newCurrentHp: number | undefined;
|
let newCurrentHp: number | undefined;
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import type { DomainEvent } from "./events.js";
|
import type { DomainEvent } from "./events.js";
|
||||||
import { sortByInitiative } from "./initiative-sort.js";
|
import { sortByInitiative } from "./initiative-sort.js";
|
||||||
import type { CombatantId, DomainError, Encounter } from "./types.js";
|
import {
|
||||||
|
type CombatantId,
|
||||||
|
type DomainError,
|
||||||
|
type Encounter,
|
||||||
|
findCombatant,
|
||||||
|
isDomainError,
|
||||||
|
} from "./types.js";
|
||||||
|
|
||||||
export interface SetInitiativeSuccess {
|
export interface SetInitiativeSuccess {
|
||||||
readonly encounter: Encounter;
|
readonly encounter: Encounter;
|
||||||
@@ -24,15 +30,8 @@ export function setInitiative(
|
|||||||
combatantId: CombatantId,
|
combatantId: CombatantId,
|
||||||
value: number | undefined,
|
value: number | undefined,
|
||||||
): SetInitiativeSuccess | DomainError {
|
): SetInitiativeSuccess | DomainError {
|
||||||
const targetIdx = encounter.combatants.findIndex((c) => c.id === combatantId);
|
const found = findCombatant(encounter, combatantId);
|
||||||
|
if (isDomainError(found)) return found;
|
||||||
if (targetIdx === -1) {
|
|
||||||
return {
|
|
||||||
kind: "domain-error",
|
|
||||||
code: "combatant-not-found",
|
|
||||||
message: `No combatant found with ID "${combatantId}"`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value !== undefined && !Number.isInteger(value)) {
|
if (value !== undefined && !Number.isInteger(value)) {
|
||||||
return {
|
return {
|
||||||
@@ -42,8 +41,7 @@ export function setInitiative(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const target = encounter.combatants[targetIdx];
|
const previousValue = found.combatant.initiative;
|
||||||
const previousValue = target.initiative;
|
|
||||||
|
|
||||||
// Create new combatants array with updated initiative
|
// Create new combatants array with updated initiative
|
||||||
const updated = encounter.combatants.map((c) =>
|
const updated = encounter.combatants.map((c) =>
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import type { DomainEvent } from "./events.js";
|
import type { DomainEvent } from "./events.js";
|
||||||
import type { CombatantId, DomainError, Encounter } from "./types.js";
|
import {
|
||||||
|
type CombatantId,
|
||||||
|
type DomainError,
|
||||||
|
type Encounter,
|
||||||
|
findCombatant,
|
||||||
|
isDomainError,
|
||||||
|
} from "./types.js";
|
||||||
|
|
||||||
export interface SetTempHpSuccess {
|
export interface SetTempHpSuccess {
|
||||||
readonly encounter: Encounter;
|
readonly encounter: Encounter;
|
||||||
@@ -18,17 +24,9 @@ export function setTempHp(
|
|||||||
combatantId: CombatantId,
|
combatantId: CombatantId,
|
||||||
tempHp: number | undefined,
|
tempHp: number | undefined,
|
||||||
): SetTempHpSuccess | DomainError {
|
): SetTempHpSuccess | DomainError {
|
||||||
const targetIdx = encounter.combatants.findIndex((c) => c.id === combatantId);
|
const found = findCombatant(encounter, combatantId);
|
||||||
|
if (isDomainError(found)) return found;
|
||||||
if (targetIdx === -1) {
|
const { combatant: target } = found;
|
||||||
return {
|
|
||||||
kind: "domain-error",
|
|
||||||
code: "combatant-not-found",
|
|
||||||
message: `No combatant found with ID "${combatantId}"`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const target = encounter.combatants[targetIdx];
|
|
||||||
|
|
||||||
if (target.maxHp === undefined || target.currentHp === undefined) {
|
if (target.maxHp === undefined || target.currentHp === undefined) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import type { DomainEvent } from "./events.js";
|
import type { DomainEvent } from "./events.js";
|
||||||
import type { CombatantId, DomainError, Encounter } from "./types.js";
|
import {
|
||||||
|
type CombatantId,
|
||||||
|
type DomainError,
|
||||||
|
type Encounter,
|
||||||
|
findCombatant,
|
||||||
|
isDomainError,
|
||||||
|
} from "./types.js";
|
||||||
|
|
||||||
export interface ToggleConcentrationSuccess {
|
export interface ToggleConcentrationSuccess {
|
||||||
readonly encounter: Encounter;
|
readonly encounter: Encounter;
|
||||||
@@ -10,17 +16,9 @@ export function toggleConcentration(
|
|||||||
encounter: Encounter,
|
encounter: Encounter,
|
||||||
combatantId: CombatantId,
|
combatantId: CombatantId,
|
||||||
): ToggleConcentrationSuccess | DomainError {
|
): ToggleConcentrationSuccess | DomainError {
|
||||||
const targetIdx = encounter.combatants.findIndex((c) => c.id === combatantId);
|
const found = findCombatant(encounter, combatantId);
|
||||||
|
if (isDomainError(found)) return found;
|
||||||
if (targetIdx === -1) {
|
const { combatant: target } = found;
|
||||||
return {
|
|
||||||
kind: "domain-error",
|
|
||||||
code: "combatant-not-found",
|
|
||||||
message: `No combatant found with ID "${combatantId}"`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const target = encounter.combatants[targetIdx];
|
|
||||||
const wasConcentrating = target.isConcentrating === true;
|
const wasConcentrating = target.isConcentrating === true;
|
||||||
|
|
||||||
const event: DomainEvent = wasConcentrating
|
const event: DomainEvent = wasConcentrating
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import type { ConditionId } from "./conditions.js";
|
import type { ConditionId } from "./conditions.js";
|
||||||
import { CONDITION_DEFINITIONS, VALID_CONDITION_IDS } from "./conditions.js";
|
import { CONDITION_DEFINITIONS, VALID_CONDITION_IDS } from "./conditions.js";
|
||||||
import type { DomainEvent } from "./events.js";
|
import type { DomainEvent } from "./events.js";
|
||||||
import type { CombatantId, DomainError, Encounter } from "./types.js";
|
import {
|
||||||
|
type CombatantId,
|
||||||
|
type DomainError,
|
||||||
|
type Encounter,
|
||||||
|
findCombatant,
|
||||||
|
isDomainError,
|
||||||
|
} from "./types.js";
|
||||||
|
|
||||||
export interface ToggleConditionSuccess {
|
export interface ToggleConditionSuccess {
|
||||||
readonly encounter: Encounter;
|
readonly encounter: Encounter;
|
||||||
@@ -21,17 +27,9 @@ export function toggleCondition(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetIdx = encounter.combatants.findIndex((c) => c.id === combatantId);
|
const found = findCombatant(encounter, combatantId);
|
||||||
|
if (isDomainError(found)) return found;
|
||||||
if (targetIdx === -1) {
|
const { combatant: target } = found;
|
||||||
return {
|
|
||||||
kind: "domain-error",
|
|
||||||
code: "combatant-not-found",
|
|
||||||
message: `No combatant found with ID "${combatantId}"`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const target = encounter.combatants[targetIdx];
|
|
||||||
const current = target.conditions ?? [];
|
const current = target.conditions ?? [];
|
||||||
const isActive = current.includes(conditionId);
|
const isActive = current.includes(conditionId);
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,20 @@ export function createEncounter(
|
|||||||
return { combatants, activeIndex, roundNumber };
|
return { combatants, activeIndex, roundNumber };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function findCombatant(
|
||||||
|
encounter: Encounter,
|
||||||
|
id: CombatantId,
|
||||||
|
): { index: number; combatant: Combatant } | DomainError {
|
||||||
|
const index = encounter.combatants.findIndex((c) => c.id === id);
|
||||||
|
if (index === -1) {
|
||||||
|
return domainError(
|
||||||
|
"combatant-not-found",
|
||||||
|
`No combatant found with ID "${id}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return { index, combatant: encounter.combatants[index] };
|
||||||
|
}
|
||||||
|
|
||||||
export function isDomainError(value: unknown): value is DomainError {
|
export function isDomainError(value: unknown): value is DomainError {
|
||||||
return (
|
return (
|
||||||
typeof value === "object" &&
|
typeof value === "object" &&
|
||||||
|
|||||||
119
pnpm-lock.yaml
generated
119
pnpm-lock.yaml
generated
@@ -21,6 +21,9 @@ importers:
|
|||||||
jscpd:
|
jscpd:
|
||||||
specifier: ^4.0.8
|
specifier: ^4.0.8
|
||||||
version: 4.0.8
|
version: 4.0.8
|
||||||
|
jsinspect-plus:
|
||||||
|
specifier: ^3.1.3
|
||||||
|
version: 3.1.3
|
||||||
knip:
|
knip:
|
||||||
specifier: ^5.88.1
|
specifier: ^5.88.1
|
||||||
version: 5.88.1(@types/node@25.3.3)(typescript@5.9.3)
|
version: 5.88.1(@types/node@25.3.3)(typescript@5.9.3)
|
||||||
@@ -133,15 +136,28 @@ packages:
|
|||||||
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
|
resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
'@babel/helper-string-parser@8.0.0-rc.3':
|
||||||
|
resolution: {integrity: sha512-AmwWFx1m8G/a5cXkxLxTiWl+YEoWuoFLUCwqMlNuWO1tqAYITQAbCRPUkyBHv1VOFgfjVOqEj6L3u15J5ZCzTA==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
|
||||||
'@babel/helper-validator-identifier@7.28.5':
|
'@babel/helper-validator-identifier@7.28.5':
|
||||||
resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
|
resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
'@babel/helper-validator-identifier@8.0.0-rc.3':
|
||||||
|
resolution: {integrity: sha512-8AWCJ2VJJyDFlGBep5GpaaQ9AAaE/FjAcrqI7jyssYhtL7WGV0DOKpJsQqM037xDbpRLHXsY8TwU7zDma7coOw==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
|
||||||
'@babel/parser@7.29.0':
|
'@babel/parser@7.29.0':
|
||||||
resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==}
|
resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
'@babel/parser@8.0.0-rc.3':
|
||||||
|
resolution: {integrity: sha512-B20dvP3MfNc/XS5KKCHy/oyWl5IA6Cn9YjXRdDlCjNmUFrjvLXMNUfQq/QUy9fnG2gYkKKcrto2YaF9B32ToOQ==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
'@babel/runtime@7.28.6':
|
'@babel/runtime@7.28.6':
|
||||||
resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==}
|
resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
@@ -150,6 +166,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
|
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
'@babel/types@8.0.0-rc.3':
|
||||||
|
resolution: {integrity: sha512-mOm5ZrYmphGfqVWoH5YYMTITb3cDXsFgmvFlvkvWDMsR9X8RFnt7a0Wb6yNIdoFsiMO9WjYLq+U/FMtqIYAF8Q==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
|
||||||
'@bcoe/v8-coverage@1.0.2':
|
'@bcoe/v8-coverage@1.0.2':
|
||||||
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
|
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -898,6 +918,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
ansi-styles@3.2.1:
|
||||||
|
resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
ansi-styles@5.2.0:
|
ansi-styles@5.2.0:
|
||||||
resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
|
resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -956,6 +980,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
|
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
chalk@2.4.2:
|
||||||
|
resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
character-parser@2.2.0:
|
character-parser@2.2.0:
|
||||||
resolution: {integrity: sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==}
|
resolution: {integrity: sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==}
|
||||||
|
|
||||||
@@ -970,10 +998,19 @@ packages:
|
|||||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
color-convert@1.9.3:
|
||||||
|
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
|
||||||
|
|
||||||
|
color-name@1.1.3:
|
||||||
|
resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==}
|
||||||
|
|
||||||
colors@1.4.0:
|
colors@1.4.0:
|
||||||
resolution: {integrity: sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==}
|
resolution: {integrity: sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==}
|
||||||
engines: {node: '>=0.1.90'}
|
engines: {node: '>=0.1.90'}
|
||||||
|
|
||||||
|
commander@2.20.3:
|
||||||
|
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
|
||||||
|
|
||||||
commander@5.1.0:
|
commander@5.1.0:
|
||||||
resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==}
|
resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
@@ -1055,6 +1092,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
|
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
escape-string-regexp@1.0.5:
|
||||||
|
resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
|
||||||
|
engines: {node: '>=0.8.0'}
|
||||||
|
|
||||||
estree-walker@3.0.3:
|
estree-walker@3.0.3:
|
||||||
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
|
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
|
||||||
|
|
||||||
@@ -1088,6 +1129,9 @@ packages:
|
|||||||
picomatch:
|
picomatch:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
filepaths@0.3.0:
|
||||||
|
resolution: {integrity: sha512-QFAYdzHZxWfBOHtHIlZySPAej+pxz6c2TGe8LGgHQNsgxHmcfbbQfNmsIh0kaangjL+6D6g8IoR6VDnOFrLEFw==}
|
||||||
|
|
||||||
fill-range@7.1.1:
|
fill-range@7.1.1:
|
||||||
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
|
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -1136,6 +1180,10 @@ packages:
|
|||||||
graceful-fs@4.2.11:
|
graceful-fs@4.2.11:
|
||||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||||
|
|
||||||
|
has-flag@3.0.0:
|
||||||
|
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
has-flag@4.0.0:
|
has-flag@4.0.0:
|
||||||
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -1251,6 +1299,10 @@ packages:
|
|||||||
canvas:
|
canvas:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
jsinspect-plus@3.1.3:
|
||||||
|
resolution: {integrity: sha512-0GbLXDlfz9nPuybM/QunzEYKTwaETxGJ5+V7vZFS7+l8w426ePVU77dBH6k+KrxiJemIgVwY6Yxr3PCzFJwxgw==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
jsonfile@6.2.0:
|
jsonfile@6.2.0:
|
||||||
resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==}
|
resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==}
|
||||||
|
|
||||||
@@ -1650,6 +1702,10 @@ packages:
|
|||||||
spark-md5@3.0.2:
|
spark-md5@3.0.2:
|
||||||
resolution: {integrity: sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==}
|
resolution: {integrity: sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==}
|
||||||
|
|
||||||
|
stable@0.1.8:
|
||||||
|
resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==}
|
||||||
|
deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility'
|
||||||
|
|
||||||
stackback@0.0.2:
|
stackback@0.0.2:
|
||||||
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
|
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
|
||||||
|
|
||||||
@@ -1672,10 +1728,18 @@ packages:
|
|||||||
resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
|
resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
strip-json-comments@3.1.1:
|
||||||
|
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
strip-json-comments@5.0.3:
|
strip-json-comments@5.0.3:
|
||||||
resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==}
|
resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==}
|
||||||
engines: {node: '>=14.16'}
|
engines: {node: '>=14.16'}
|
||||||
|
|
||||||
|
supports-color@5.5.0:
|
||||||
|
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
supports-color@7.2.0:
|
supports-color@7.2.0:
|
||||||
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -1921,12 +1985,20 @@ snapshots:
|
|||||||
|
|
||||||
'@babel/helper-string-parser@7.27.1': {}
|
'@babel/helper-string-parser@7.27.1': {}
|
||||||
|
|
||||||
|
'@babel/helper-string-parser@8.0.0-rc.3': {}
|
||||||
|
|
||||||
'@babel/helper-validator-identifier@7.28.5': {}
|
'@babel/helper-validator-identifier@7.28.5': {}
|
||||||
|
|
||||||
|
'@babel/helper-validator-identifier@8.0.0-rc.3': {}
|
||||||
|
|
||||||
'@babel/parser@7.29.0':
|
'@babel/parser@7.29.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/types': 7.29.0
|
'@babel/types': 7.29.0
|
||||||
|
|
||||||
|
'@babel/parser@8.0.0-rc.3':
|
||||||
|
dependencies:
|
||||||
|
'@babel/types': 8.0.0-rc.3
|
||||||
|
|
||||||
'@babel/runtime@7.28.6': {}
|
'@babel/runtime@7.28.6': {}
|
||||||
|
|
||||||
'@babel/types@7.29.0':
|
'@babel/types@7.29.0':
|
||||||
@@ -1934,6 +2006,11 @@ snapshots:
|
|||||||
'@babel/helper-string-parser': 7.27.1
|
'@babel/helper-string-parser': 7.27.1
|
||||||
'@babel/helper-validator-identifier': 7.28.5
|
'@babel/helper-validator-identifier': 7.28.5
|
||||||
|
|
||||||
|
'@babel/types@8.0.0-rc.3':
|
||||||
|
dependencies:
|
||||||
|
'@babel/helper-string-parser': 8.0.0-rc.3
|
||||||
|
'@babel/helper-validator-identifier': 8.0.0-rc.3
|
||||||
|
|
||||||
'@bcoe/v8-coverage@1.0.2': {}
|
'@bcoe/v8-coverage@1.0.2': {}
|
||||||
|
|
||||||
'@biomejs/biome@2.4.8':
|
'@biomejs/biome@2.4.8':
|
||||||
@@ -2481,6 +2558,10 @@ snapshots:
|
|||||||
|
|
||||||
ansi-regex@5.0.1: {}
|
ansi-regex@5.0.1: {}
|
||||||
|
|
||||||
|
ansi-styles@3.2.1:
|
||||||
|
dependencies:
|
||||||
|
color-convert: 1.9.3
|
||||||
|
|
||||||
ansi-styles@5.2.0: {}
|
ansi-styles@5.2.0: {}
|
||||||
|
|
||||||
aria-query@5.3.0:
|
aria-query@5.3.0:
|
||||||
@@ -2534,6 +2615,12 @@ snapshots:
|
|||||||
|
|
||||||
chai@6.2.2: {}
|
chai@6.2.2: {}
|
||||||
|
|
||||||
|
chalk@2.4.2:
|
||||||
|
dependencies:
|
||||||
|
ansi-styles: 3.2.1
|
||||||
|
escape-string-regexp: 1.0.5
|
||||||
|
supports-color: 5.5.0
|
||||||
|
|
||||||
character-parser@2.2.0:
|
character-parser@2.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-regex: 1.2.1
|
is-regex: 1.2.1
|
||||||
@@ -2550,8 +2637,16 @@ snapshots:
|
|||||||
|
|
||||||
clsx@2.1.1: {}
|
clsx@2.1.1: {}
|
||||||
|
|
||||||
|
color-convert@1.9.3:
|
||||||
|
dependencies:
|
||||||
|
color-name: 1.1.3
|
||||||
|
|
||||||
|
color-name@1.1.3: {}
|
||||||
|
|
||||||
colors@1.4.0: {}
|
colors@1.4.0: {}
|
||||||
|
|
||||||
|
commander@2.20.3: {}
|
||||||
|
|
||||||
commander@5.1.0: {}
|
commander@5.1.0: {}
|
||||||
|
|
||||||
constantinople@4.0.1:
|
constantinople@4.0.1:
|
||||||
@@ -2624,6 +2719,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
|
|
||||||
|
escape-string-regexp@1.0.5: {}
|
||||||
|
|
||||||
estree-walker@3.0.3:
|
estree-walker@3.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.8
|
'@types/estree': 1.0.8
|
||||||
@@ -2664,6 +2761,8 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
picomatch: 4.0.4
|
picomatch: 4.0.4
|
||||||
|
|
||||||
|
filepaths@0.3.0: {}
|
||||||
|
|
||||||
fill-range@7.1.1:
|
fill-range@7.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
to-regex-range: 5.0.1
|
to-regex-range: 5.0.1
|
||||||
@@ -2715,6 +2814,8 @@ snapshots:
|
|||||||
|
|
||||||
graceful-fs@4.2.11: {}
|
graceful-fs@4.2.11: {}
|
||||||
|
|
||||||
|
has-flag@3.0.0: {}
|
||||||
|
|
||||||
has-flag@4.0.0: {}
|
has-flag@4.0.0: {}
|
||||||
|
|
||||||
has-symbols@1.1.0: {}
|
has-symbols@1.1.0: {}
|
||||||
@@ -2841,6 +2942,16 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@noble/hashes'
|
- '@noble/hashes'
|
||||||
|
|
||||||
|
jsinspect-plus@3.1.3:
|
||||||
|
dependencies:
|
||||||
|
'@babel/parser': 8.0.0-rc.3
|
||||||
|
chalk: 2.4.2
|
||||||
|
commander: 2.20.3
|
||||||
|
filepaths: 0.3.0
|
||||||
|
stable: 0.1.8
|
||||||
|
strip-indent: 3.0.0
|
||||||
|
strip-json-comments: 3.1.1
|
||||||
|
|
||||||
jsonfile@6.2.0:
|
jsonfile@6.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
universalify: 2.0.1
|
universalify: 2.0.1
|
||||||
@@ -3268,6 +3379,8 @@ snapshots:
|
|||||||
|
|
||||||
spark-md5@3.0.2: {}
|
spark-md5@3.0.2: {}
|
||||||
|
|
||||||
|
stable@0.1.8: {}
|
||||||
|
|
||||||
stackback@0.0.2: {}
|
stackback@0.0.2: {}
|
||||||
|
|
||||||
std-env@4.0.0: {}
|
std-env@4.0.0: {}
|
||||||
@@ -3288,8 +3401,14 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
min-indent: 1.0.1
|
min-indent: 1.0.1
|
||||||
|
|
||||||
|
strip-json-comments@3.1.1: {}
|
||||||
|
|
||||||
strip-json-comments@5.0.3: {}
|
strip-json-comments@5.0.3: {}
|
||||||
|
|
||||||
|
supports-color@5.5.0:
|
||||||
|
dependencies:
|
||||||
|
has-flag: 3.0.0
|
||||||
|
|
||||||
supports-color@7.2.0:
|
supports-color@7.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
has-flag: 4.0.0
|
has-flag: 4.0.0
|
||||||
|
|||||||
Reference in New Issue
Block a user