From b88fa0cb3ea804e52e7979420634e5568e766836 Mon Sep 17 00:00:00 2001 From: Aaron Wood Date: Thu, 9 Apr 2026 21:35:49 -0400 Subject: [PATCH] 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) --- .../src/components/CharacterSheet.module.css | 31 ++++++++++++++ client/src/components/CharacterSheet.tsx | 40 ++++++++++++++++--- client/src/components/DiceTray.tsx | 9 +++-- client/src/components/StatBlock.module.css | 27 ------------- client/src/components/StatBlock.tsx | 28 +++++-------- client/src/types.ts | 1 + server/src/db.ts | 1 + server/src/routes/characters.ts | 2 + 8 files changed, 87 insertions(+), 52 deletions(-) diff --git a/client/src/components/CharacterSheet.module.css b/client/src/components/CharacterSheet.module.css index 89d8432..6239766 100644 --- a/client/src/components/CharacterSheet.module.css +++ b/client/src/components/CharacterSheet.module.css @@ -104,6 +104,37 @@ 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 { font-family: "Cinzel", Georgia, serif; font-size: 1.3rem; diff --git a/client/src/components/CharacterSheet.tsx b/client/src/components/CharacterSheet.tsx index b1192c5..fdd8a58 100644 --- a/client/src/components/CharacterSheet.tsx +++ b/client/src/components/CharacterSheet.tsx @@ -89,11 +89,22 @@ export default function CharacterSheet({
{mode === "edit" ? (
- handleNameField("name", e.target.value)} - /> +
+ handleNameField("name", e.target.value)} + /> + + onUpdate(character.id, { color: e.target.value }) + } + title="Character color" + /> +
/ {xpThreshold}
+ + {/* Luck token — always toggleable */} +
+ +
diff --git a/client/src/components/DiceTray.tsx b/client/src/components/DiceTray.tsx index 51ee3f9..9cc3da3 100644 --- a/client/src/components/DiceTray.tsx +++ b/client/src/components/DiceTray.tsx @@ -138,9 +138,12 @@ function buildNotation(expression: string, rolls: number[]): string | null { return `${count}d${sides}@${values}`; } -/** Convert an HSL string like "hsl(210, 70%, 50%)" to a hex color */ -function hslToHex(hsl: string): string { - const match = hsl.match(/hsl\((\d+),\s*(\d+)%,\s*(\d+)%\)/); +/** Convert a color string (HSL or hex) to hex */ +function hslToHex(color: string): string { + // Already hex — pass through + if (color.startsWith("#")) return color; + + const match = color.match(/hsl\((\d+),\s*(\d+)%,\s*(\d+)%\)/); if (!match) return "#8B2020"; const h = parseInt(match[1]) / 360; diff --git a/client/src/components/StatBlock.module.css b/client/src/components/StatBlock.module.css index 5ed388f..3169da7 100644 --- a/client/src/components/StatBlock.module.css +++ b/client/src/components/StatBlock.module.css @@ -54,30 +54,3 @@ color: var(--hp); 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; -} diff --git a/client/src/components/StatBlock.tsx b/client/src/components/StatBlock.tsx index c96b3fa..d181c14 100644 --- a/client/src/components/StatBlock.tsx +++ b/client/src/components/StatBlock.tsx @@ -1,5 +1,6 @@ import type { Stat } from "../types"; import { getModifier, formatModifier } from "../utils/modifiers"; +import InlineNumber from "./InlineNumber"; import DiceButton from "./DiceButton"; import styles from "./StatBlock.module.css"; @@ -39,14 +40,6 @@ export default function StatBlock({
{name}
- {mode === "edit" && ( - - )} {formatModifier(mod)} {mode === "view" && campaignId && ( )} - {mode === "edit" && ( - - )}
- {value} + {mode === "edit" ? ( + onStatChange(name, v)} + min={1} + max={30} + /> + ) : ( + value + )} {bonus > 0 && (+{bonus})}
diff --git a/client/src/types.ts b/client/src/types.ts index 16d44fc..7b33926 100644 --- a/client/src/types.ts +++ b/client/src/types.ts @@ -54,6 +54,7 @@ export interface Character { gear_slots_max: number; overrides: Record; color: string; + luck_token: number; stats: Stat[]; gear: Gear[]; talents: Talent[]; diff --git a/server/src/db.ts b/server/src/db.ts index 5c265bf..eb19117 100644 --- a/server/src/db.ts +++ b/server/src/db.ts @@ -111,6 +111,7 @@ const v2Columns: Array<[string, string, string]> = [ ["character_gear", "effects", "TEXT DEFAULT '{}'"], ["character_talents", "game_talent_id", "INTEGER"], ["characters", "color", "TEXT DEFAULT ''"], + ["characters", "luck_token", "INTEGER DEFAULT 1"], ["roll_log", "nat20", "INTEGER DEFAULT 0"], ["roll_log", "character_color", "TEXT DEFAULT ''"], ]; diff --git a/server/src/routes/characters.ts b/server/src/routes/characters.ts index fe59c35..d5a2863 100644 --- a/server/src/routes/characters.ts +++ b/server/src/routes/characters.ts @@ -148,6 +148,8 @@ router.patch("/:id", (req, res) => { "cp", "gear_slots_max", "overrides", + "color", + "luck_token", ]; const updates: string[] = [];