Add luck token toggle, color picker, and click-to-edit ability scores
- Luck token: star toggle (filled/empty) in header, always clickable - Color picker: input[type=color] next to name in edit mode - Ability scores: InlineNumber click-to-edit replaces +/- buttons - Server: added color and luck_token to allowed update fields - DB: luck_token column migration (default 1) - Fix dice color for hex color values (was only handling HSL) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
51ffb0033a
commit
b88fa0cb3e
8 changed files with 87 additions and 52 deletions
|
|
@ -104,6 +104,37 @@
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.luckBtn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0.1rem;
|
||||||
|
transition: transform 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.luckBtn:hover {
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorPicker {
|
||||||
|
width: 2rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
border: 1px solid rgba(var(--gold-rgb), 0.3);
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colorRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
margin-top: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
.nameInput {
|
.nameInput {
|
||||||
font-family: "Cinzel", Georgia, serif;
|
font-family: "Cinzel", Georgia, serif;
|
||||||
font-size: 1.3rem;
|
font-size: 1.3rem;
|
||||||
|
|
|
||||||
|
|
@ -89,11 +89,22 @@ export default function CharacterSheet({
|
||||||
<div className={styles.identity}>
|
<div className={styles.identity}>
|
||||||
{mode === "edit" ? (
|
{mode === "edit" ? (
|
||||||
<div>
|
<div>
|
||||||
<input
|
<div className={styles.colorRow}>
|
||||||
className={styles.nameInput}
|
<input
|
||||||
defaultValue={character.name}
|
className={styles.nameInput}
|
||||||
onChange={(e) => handleNameField("name", e.target.value)}
|
defaultValue={character.name}
|
||||||
/>
|
onChange={(e) => handleNameField("name", e.target.value)}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
className={styles.colorPicker}
|
||||||
|
value={character.color || "#888888"}
|
||||||
|
onChange={(e) =>
|
||||||
|
onUpdate(character.id, { color: e.target.value })
|
||||||
|
}
|
||||||
|
title="Character color"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<input
|
<input
|
||||||
className={styles.titleInput}
|
className={styles.titleInput}
|
||||||
defaultValue={character.title}
|
defaultValue={character.title}
|
||||||
|
|
@ -169,6 +180,25 @@ export default function CharacterSheet({
|
||||||
<span className={styles.xpThreshold}>/ {xpThreshold}</span>
|
<span className={styles.xpThreshold}>/ {xpThreshold}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Luck token — always toggleable */}
|
||||||
|
<div className={styles.vital}>
|
||||||
|
<button
|
||||||
|
className={styles.luckBtn}
|
||||||
|
onClick={() =>
|
||||||
|
onUpdate(character.id, {
|
||||||
|
luck_token: character.luck_token ? 0 : 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
title={
|
||||||
|
character.luck_token
|
||||||
|
? "Luck token available"
|
||||||
|
: "Luck token spent"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{character.luck_token ? "\u2605" : "\u2606"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -138,9 +138,12 @@ function buildNotation(expression: string, rolls: number[]): string | null {
|
||||||
return `${count}d${sides}@${values}`;
|
return `${count}d${sides}@${values}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Convert an HSL string like "hsl(210, 70%, 50%)" to a hex color */
|
/** Convert a color string (HSL or hex) to hex */
|
||||||
function hslToHex(hsl: string): string {
|
function hslToHex(color: string): string {
|
||||||
const match = hsl.match(/hsl\((\d+),\s*(\d+)%,\s*(\d+)%\)/);
|
// Already hex — pass through
|
||||||
|
if (color.startsWith("#")) return color;
|
||||||
|
|
||||||
|
const match = color.match(/hsl\((\d+),\s*(\d+)%,\s*(\d+)%\)/);
|
||||||
if (!match) return "#8B2020";
|
if (!match) return "#8B2020";
|
||||||
|
|
||||||
const h = parseInt(match[1]) / 360;
|
const h = parseInt(match[1]) / 360;
|
||||||
|
|
|
||||||
|
|
@ -54,30 +54,3 @@
|
||||||
color: var(--hp);
|
color: var(--hp);
|
||||||
font-size: 0.65rem;
|
font-size: 0.65rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
|
||||||
width: 22px;
|
|
||||||
height: 22px;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 1px solid rgba(var(--gold-rgb), 0.25);
|
|
||||||
background: var(--bg-inset);
|
|
||||||
color: var(--text-primary);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
line-height: 1;
|
|
||||||
transition:
|
|
||||||
border-color 0.15s,
|
|
||||||
color 0.15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn:hover {
|
|
||||||
border-color: var(--gold);
|
|
||||||
color: var(--gold);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rollSpace {
|
|
||||||
width: 2.5rem;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import type { Stat } from "../types";
|
import type { Stat } from "../types";
|
||||||
import { getModifier, formatModifier } from "../utils/modifiers";
|
import { getModifier, formatModifier } from "../utils/modifiers";
|
||||||
|
import InlineNumber from "./InlineNumber";
|
||||||
import DiceButton from "./DiceButton";
|
import DiceButton from "./DiceButton";
|
||||||
import styles from "./StatBlock.module.css";
|
import styles from "./StatBlock.module.css";
|
||||||
|
|
||||||
|
|
@ -39,14 +40,6 @@ export default function StatBlock({
|
||||||
<div key={name} className={styles.stat}>
|
<div key={name} className={styles.stat}>
|
||||||
<span className={styles.statName}>{name}</span>
|
<span className={styles.statName}>{name}</span>
|
||||||
<div className={styles.statRow}>
|
<div className={styles.statRow}>
|
||||||
{mode === "edit" && (
|
|
||||||
<button
|
|
||||||
className={styles.btn}
|
|
||||||
onClick={() => onStatChange(name, baseValue - 1)}
|
|
||||||
>
|
|
||||||
−
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<span className={styles.modifier}>{formatModifier(mod)}</span>
|
<span className={styles.modifier}>{formatModifier(mod)}</span>
|
||||||
{mode === "view" && campaignId && (
|
{mode === "view" && campaignId && (
|
||||||
<DiceButton
|
<DiceButton
|
||||||
|
|
@ -59,17 +52,18 @@ export default function StatBlock({
|
||||||
label={`${name} check`}
|
label={`${name} check`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{mode === "edit" && (
|
|
||||||
<button
|
|
||||||
className={styles.btn}
|
|
||||||
onClick={() => onStatChange(name, baseValue + 1)}
|
|
||||||
>
|
|
||||||
+
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<span className={styles.score}>
|
<span className={styles.score}>
|
||||||
{value}
|
{mode === "edit" ? (
|
||||||
|
<InlineNumber
|
||||||
|
value={baseValue}
|
||||||
|
onChange={(v) => onStatChange(name, v)}
|
||||||
|
min={1}
|
||||||
|
max={30}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
value
|
||||||
|
)}
|
||||||
{bonus > 0 && <span className={styles.bonus}> (+{bonus})</span>}
|
{bonus > 0 && <span className={styles.bonus}> (+{bonus})</span>}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ export interface Character {
|
||||||
gear_slots_max: number;
|
gear_slots_max: number;
|
||||||
overrides: Record<string, unknown>;
|
overrides: Record<string, unknown>;
|
||||||
color: string;
|
color: string;
|
||||||
|
luck_token: number;
|
||||||
stats: Stat[];
|
stats: Stat[];
|
||||||
gear: Gear[];
|
gear: Gear[];
|
||||||
talents: Talent[];
|
talents: Talent[];
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,7 @@ const v2Columns: Array<[string, string, string]> = [
|
||||||
["character_gear", "effects", "TEXT DEFAULT '{}'"],
|
["character_gear", "effects", "TEXT DEFAULT '{}'"],
|
||||||
["character_talents", "game_talent_id", "INTEGER"],
|
["character_talents", "game_talent_id", "INTEGER"],
|
||||||
["characters", "color", "TEXT DEFAULT ''"],
|
["characters", "color", "TEXT DEFAULT ''"],
|
||||||
|
["characters", "luck_token", "INTEGER DEFAULT 1"],
|
||||||
["roll_log", "nat20", "INTEGER DEFAULT 0"],
|
["roll_log", "nat20", "INTEGER DEFAULT 0"],
|
||||||
["roll_log", "character_color", "TEXT DEFAULT ''"],
|
["roll_log", "character_color", "TEXT DEFAULT ''"],
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -148,6 +148,8 @@ router.patch("/:id", (req, res) => {
|
||||||
"cp",
|
"cp",
|
||||||
"gear_slots_max",
|
"gear_slots_max",
|
||||||
"overrides",
|
"overrides",
|
||||||
|
"color",
|
||||||
|
"luck_token",
|
||||||
];
|
];
|
||||||
|
|
||||||
const updates: string[] = [];
|
const updates: string[] = [];
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue