feat: Add skill template picker to Classes page

Mirrors the spell template picker: pack skills.yml into the sheet and
let each class insert a formatted skill description from the book,
filtered to that class by default.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-04 12:37:17 -04:00
parent bdd7d03d9c
commit 2063fffa29
3 changed files with 527 additions and 19 deletions

View File

@@ -1,7 +1,14 @@
import React from "react";
import React, { useState } from "react";
import skillsFile from "../../data/skills.yml";
import { ClassEntry } from "../types";
import { autoResize } from "../utils";
type SkillPickerTarget = { list: "primary" | "other"; idx: number };
function formatSkillLine(t: SkillTemplate): string {
return `${t.name} (${t.class}, max SL ${t.max_level}): ${t.description}`;
}
interface ClassesPageProps {
isActive: boolean;
primaryClasses: ClassEntry[];
@@ -21,6 +28,29 @@ export default function ClassesPage({
heroicSkills,
onHeroicSkillsChange,
}: ClassesPageProps) {
const [skillPickerTarget, setSkillPickerTarget] = useState<SkillPickerTarget | null>(null);
const [skillCategory, setSkillCategory] = useState<string>("all");
const allSkills = (skillsFile as SkillsFile).skills;
const skillCategories = ["all", ...Array.from(new Set(allSkills.map((s) => s.class))).sort()];
const visibleSkills =
skillCategory === "all" ? allSkills : allSkills.filter((s) => s.class === skillCategory);
const appendSkill = (target: SkillPickerTarget, line: string) => {
const setter = target.list === "primary" ? setPrimaryClasses : setOtherClasses;
setter((prev) =>
prev.map((c, i) =>
i === target.idx ? { ...c, skills: c.skills ? c.skills + "\n" + line : line } : c
)
);
};
const openSkillPicker = (target: SkillPickerTarget, className: string) => {
const match = allSkills.find((s) => s.class.toLowerCase() === className.trim().toLowerCase());
setSkillCategory(match ? match.class : "all");
setSkillPickerTarget(target);
};
return (
<div className={`page${isActive ? " active" : ""}`} id="page-classes">
<div className="grid-2" style={{ marginBottom: 20 }}>
@@ -95,15 +125,25 @@ export default function ClassesPage({
style={{ width: "50px" }}
/>
</label>
<button
type="button"
className="spell-del-btn"
onClick={() =>
setPrimaryClasses((prev) => prev.filter((_, i) => i !== idx))
}
>
Remove
</button>
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
<button
type="button"
className="add-btn"
style={{ marginTop: 0 }}
onClick={() => openSkillPicker({ list: "primary", idx }, cls.name)}
>
+ Add Template Skill
</button>
<button
type="button"
className="spell-del-btn"
onClick={() =>
setPrimaryClasses((prev) => prev.filter((_, i) => i !== idx))
}
>
Remove
</button>
</div>
</div>
</div>
))}
@@ -189,15 +229,25 @@ export default function ClassesPage({
style={{ width: "50px" }}
/>
</label>
<button
type="button"
className="spell-del-btn"
onClick={() =>
setOtherClasses((prev) => prev.filter((_, i) => i !== idx))
}
>
Remove
</button>
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
<button
type="button"
className="add-btn"
style={{ marginTop: 0 }}
onClick={() => openSkillPicker({ list: "other", idx }, cls.name)}
>
+ Add Template Skill
</button>
<button
type="button"
className="spell-del-btn"
onClick={() =>
setOtherClasses((prev) => prev.filter((_, i) => i !== idx))
}
>
Remove
</button>
</div>
</div>
</div>
))}
@@ -227,6 +277,70 @@ export default function ClassesPage({
style={{ minHeight: 100 }}
/>
</div>
{skillPickerTarget &&
(() => {
const target = skillPickerTarget;
return (
<div
className="spell-picker-overlay"
onClick={() => setSkillPickerTarget(null)}
>
<div className="spell-picker-modal" onClick={(e) => e.stopPropagation()}>
<div className="spell-picker-header">
<span>Choose a skill</span>
<button
type="button"
className="spell-picker-close"
onClick={() => setSkillPickerTarget(null)}
>
</button>
</div>
<div
style={{
display: "flex",
gap: 6,
flexWrap: "wrap",
padding: "8px 14px",
borderBottom: "1px solid var(--border)",
}}
>
{skillCategories.map((cat) => (
<button
type="button"
key={cat}
className={`add-btn${skillCategory === cat ? " active-filter" : ""}`}
style={{
padding: "2px 8px",
fontSize: "0.65rem",
textTransform: "capitalize",
}}
onClick={() => setSkillCategory(cat)}
>
{cat}
</button>
))}
</div>
<ul className="spell-picker-list">
{visibleSkills.map((t, i) => (
<li
key={i}
className="spell-picker-item"
onClick={() => {
appendSkill(target, formatSkillLine(t));
setSkillPickerTarget(null);
}}
>
<span className="spell-picker-name">{t.name}</span>
<span className="spell-picker-class">{t.class}</span>
</li>
))}
</ul>
</div>
</div>
);
})()}
</div>
);
}