3 Commits

Author SHA1 Message Date
Lukas
228a2603e8 Add Sapped and Slowed conditions for 5.5e weapon mastery
All checks were successful
CI / check (push) Successful in 1m8s
CI / build-image (push) Successful in 15s
These D&D 2024 weapon mastery conditions are edition-gated: they only
appear in the condition picker when 5.5e rules are selected. Applied
conditions still render correctly regardless of edition setting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 00:31:41 +01:00
Lukas
27ff8ba1ad Collapse hover-only buttons to zero width when hidden
All checks were successful
CI / check (push) Successful in 1m8s
CI / build-image (push) Successful in 16s
Edit and add-condition buttons now take no space when not hovered,
eliminating the gap between name and condition icons. They slide in
smoothly on hover with a 150ms transition.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 00:19:47 +01:00
Lukas
4cfcefe6c3 Hide custom stat fields on mobile, fix action bar gap consistency
All checks were successful
CI / check (push) Successful in 1m7s
CI / build-image (push) Successful in 16s
Init/AC/MaxHP inputs are hidden on phones — users set these values
directly in the combatant row after adding. Fixes uneven spacing
between action bar elements by using consistent gap-3.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 23:41:18 +01:00
7 changed files with 90 additions and 8 deletions

View File

@@ -522,7 +522,7 @@ export function ActionBar({
<div className="card-glow flex items-center gap-3 border-border border-t bg-card px-4 py-3 sm:rounded-lg sm:border">
<form
onSubmit={handleAdd}
className="relative flex flex-1 flex-wrap items-center gap-2 sm:flex-nowrap"
className="relative flex flex-1 flex-wrap items-center gap-3 sm:flex-nowrap"
>
<div className="flex-1">
<div className="relative max-w-xs">
@@ -606,7 +606,7 @@ export function ActionBar({
</div>
</div>
{!browseMode && nameInput.length >= 2 && !hasSuggestions && (
<div className="flex items-center gap-2">
<div className="hidden items-center gap-2 sm:flex">
<Input
type="text"
inputMode="numeric"

View File

@@ -112,7 +112,7 @@ function EditableName({
onClick={startEditing}
title="Rename"
aria-label="Rename"
className="inline-flex shrink-0 items-center rounded p-0.5 text-muted-foreground opacity-0 pointer-coarse:opacity-100 transition-colors transition-opacity hover:bg-hover-neutral-bg hover:text-hover-neutral focus:opacity-100 group-hover:opacity-100"
className="inline-flex pointer-coarse:w-auto w-0 shrink-0 items-center overflow-hidden pointer-coarse:overflow-visible rounded p-0.5 text-muted-foreground opacity-0 pointer-coarse:opacity-100 transition-all duration-150 hover:bg-hover-neutral-bg hover:text-hover-neutral focus:w-auto focus:overflow-visible focus:opacity-100 group-hover:w-auto group-hover:overflow-visible group-hover:opacity-100"
>
<Pencil size={14} />
</button>

View File

@@ -1,7 +1,7 @@
import {
CONDITION_DEFINITIONS,
type ConditionId,
getConditionDescription,
getConditionsForEdition,
} from "@initiative/domain";
import type { LucideIcon } from "lucide-react";
import {
@@ -17,7 +17,9 @@ import {
Heart,
Link,
Moon,
ShieldMinus,
Siren,
Snail,
Sparkles,
ZapOff,
} from "lucide-react";
@@ -41,6 +43,8 @@ const ICON_MAP: Record<string, LucideIcon> = {
Droplet,
ArrowDown,
Link,
ShieldMinus,
Snail,
Sparkles,
Moon,
};
@@ -56,6 +60,7 @@ const COLOR_CLASSES: Record<string, string> = {
slate: "text-slate-400",
green: "text-green-400",
indigo: "text-indigo-400",
sky: "text-sky-400",
};
interface ConditionPickerProps {
@@ -110,6 +115,7 @@ export function ConditionPicker({
}, [onClose]);
const { edition } = useRulesEditionContext();
const conditions = getConditionsForEdition(edition);
const active = new Set(activeConditions ?? []);
return createPortal(
@@ -122,7 +128,7 @@ export function ConditionPicker({
: { visibility: "hidden" as const }
}
>
{CONDITION_DEFINITIONS.map((def) => {
{conditions.map((def) => {
const Icon = ICON_MAP[def.iconName];
if (!Icon) return null;
const isActive = active.has(def.id);

View File

@@ -18,7 +18,9 @@ import {
Link,
Moon,
Plus,
ShieldMinus,
Siren,
Snail,
Sparkles,
ZapOff,
} from "lucide-react";
@@ -40,6 +42,8 @@ const ICON_MAP: Record<string, LucideIcon> = {
Droplet,
ArrowDown,
Link,
ShieldMinus,
Snail,
Sparkles,
Moon,
};
@@ -55,6 +59,7 @@ const COLOR_CLASSES: Record<string, string> = {
slate: "text-slate-400",
green: "text-green-400",
indigo: "text-indigo-400",
sky: "text-sky-400",
};
interface ConditionTagsProps {
@@ -103,7 +108,7 @@ export function ConditionTags({
type="button"
title="Add condition"
aria-label="Add condition"
className="inline-flex items-center rounded p-0.5 text-muted-foreground opacity-0 pointer-coarse:opacity-100 transition-colors transition-opacity hover:bg-hover-neutral-bg hover:text-hover-neutral focus:opacity-100 group-hover:opacity-100"
className="inline-flex pointer-coarse:w-auto w-0 items-center overflow-hidden pointer-coarse:overflow-visible rounded p-0.5 text-muted-foreground opacity-0 pointer-coarse:opacity-100 transition-all duration-150 hover:bg-hover-neutral-bg hover:text-hover-neutral focus:w-auto focus:overflow-visible focus:opacity-100 group-hover:w-auto group-hover:overflow-visible group-hover:opacity-100"
onClick={(e) => {
e.stopPropagation();
onOpenPicker();

View File

@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import {
CONDITION_DEFINITIONS,
getConditionDescription,
getConditionsForEdition,
} from "../conditions.js";
function findCondition(id: string) {
@@ -25,13 +26,27 @@ describe("getConditionDescription", () => {
);
});
it("every condition has both descriptions", () => {
for (const def of CONDITION_DEFINITIONS) {
it("universal conditions have both descriptions", () => {
const universal = CONDITION_DEFINITIONS.filter(
(d) => d.edition === undefined,
);
expect(universal.length).toBeGreaterThan(0);
for (const def of universal) {
expect(def.description).toBeTruthy();
expect(def.description5e).toBeTruthy();
}
});
it("edition-specific conditions have their edition description", () => {
const sapped = findCondition("sapped");
expect(sapped.description).toBeTruthy();
expect(sapped.edition).toBe("5.5e");
const slowed = findCondition("slowed");
expect(slowed.description).toBeTruthy();
expect(slowed.edition).toBe("5.5e");
});
it("conditions with identical rules share the same text", () => {
const blinded = findCondition("blinded");
expect(blinded.description).toBe(blinded.description5e);
@@ -42,3 +57,26 @@ describe("getConditionDescription", () => {
expect(exhaustion.description).not.toBe(exhaustion.description5e);
});
});
describe("getConditionsForEdition", () => {
it("includes sapped and slowed for 5.5e", () => {
const conditions = getConditionsForEdition("5.5e");
const ids = conditions.map((d) => d.id);
expect(ids).toContain("sapped");
expect(ids).toContain("slowed");
});
it("excludes sapped and slowed for 5e", () => {
const conditions = getConditionsForEdition("5e");
const ids = conditions.map((d) => d.id);
expect(ids).not.toContain("sapped");
expect(ids).not.toContain("slowed");
});
it("includes universal conditions for both editions", () => {
const ids5e = getConditionsForEdition("5e").map((d) => d.id);
const ids55e = getConditionsForEdition("5.5e").map((d) => d.id);
expect(ids5e).toContain("blinded");
expect(ids55e).toContain("blinded");
});
});

View File

@@ -12,6 +12,8 @@ export type ConditionId =
| "poisoned"
| "prone"
| "restrained"
| "sapped"
| "slowed"
| "stunned"
| "unconscious";
@@ -24,6 +26,8 @@ export interface ConditionDefinition {
readonly description5e: string;
readonly iconName: string;
readonly color: string;
/** When set, the condition only appears in this edition's picker. */
readonly edition?: RulesEdition;
}
export function getConditionDescription(
@@ -159,6 +163,26 @@ export const CONDITION_DEFINITIONS: readonly ConditionDefinition[] = [
iconName: "Link",
color: "neutral",
},
{
id: "sapped",
label: "Sapped",
description:
"Disadvantage on next attack roll before the start of your next turn. (Weapon Mastery: Sap)",
description5e: "",
iconName: "ShieldMinus",
color: "amber",
edition: "5.5e",
},
{
id: "slowed",
label: "Slowed",
description:
"Speed reduced by 10 ft. until the start of your next turn. (Weapon Mastery: Slow)",
description5e: "",
iconName: "Snail",
color: "sky",
edition: "5.5e",
},
{
id: "stunned",
label: "Stunned",
@@ -184,3 +208,11 @@ export const CONDITION_DEFINITIONS: readonly ConditionDefinition[] = [
export const VALID_CONDITION_IDS: ReadonlySet<string> = new Set(
CONDITION_DEFINITIONS.map((d) => d.id),
);
export function getConditionsForEdition(
edition: RulesEdition,
): readonly ConditionDefinition[] {
return CONDITION_DEFINITIONS.filter(
(d) => d.edition === undefined || d.edition === edition,
);
}

View File

@@ -11,6 +11,7 @@ export {
type ConditionDefinition,
type ConditionId,
getConditionDescription,
getConditionsForEdition,
type RulesEdition,
VALID_CONDITION_IDS,
} from "./conditions.js";