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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user