Files
dm-pal/docs/ui-ux-improvements.md
2026-08-09 13:49:54 +01:00

1215 lines
54 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# DM-Pal — UI/UX Improvements
*A review of the current app shell, dashboard, and every utility. Findings are
ordered by impact, not file order. Each item ships with a concrete, checkable
recommendation.*
> **Status:** in progress — P0 and P1 shipped, P2 mostly done. Initiative,
> Dice, Encounter, Settings, Cross-cutting, Random Tables, Session Logger,
> Calendar, and Lore Panel are complete. NPC/Item/Quest have stat/structured/
> reward fields shipped; soundboard scenes + image gallery/batch shipped;
> remaining P2 items are larger-effort or backend-gated: image-gen advanced
> params (Ollama API doesn't support them), soundboard real ambience packs +
> custom sound import, world hierarchy tree, lore directory picker (needs
> tauri-plugin-dialog), NPC roster / item inventory / quest roster (History
> view covers re-opening). The checklist below is updated as items land.
---
## 1. Executive Summary
DM-Pal is a Tauri v2 desktop app for running D&D sessions with a local LLM.
The visual identity (dark navy + Cinzel headings + gold accents, glassmorphic
bento cards) is on-brand and consistent across surfaces. The architecture is
clean — every tool is a self-contained card mounted into either the bento
dashboard or a single-column detail view.
**The biggest problems right now are not aesthetic — they are interaction
shape and information density.** The dashboard tries to render *full* tool
UIs inside ~250px tiles, the left rail has grown to **14 entries** with two
identical `Sparkles` icons, and the "Expand" button is the only escape hatch
back to a usable view. There is no model-loading state, no streaming
indicator, no "thinking…" affordance, and the settings panel is a dead end
until you click "Load Settings". Detail views duplicate the same header
treatment as the dashboard and don't give the tools room to breathe.
The good news: the design system tokens, component primitives (`BentoCard`,
`Toast`, `GeneratedImage`), and Rust↔TS plumbing are all in place. Most of
the wins are layout, IA, and copy — not a redesign.
### Top 5 wins (do these first)
1.**Kill the "expand to use it" pattern.** Make every bento card a *summary*
with a clear `Open` button. Tools render full-width in the detail view,
not at 1/4 size on the dashboard.
2.**Reorganize the left rail into 2 visual groups** (Session vs World) with
separators and tooltips that show on hover for icon-only nav.
3.**Add a real loading/streaming UI** for LLM and image generation. The
"Generating…" text in a button is not enough for 520 s waits.
4.**Ship a real Settings page** (load on mount, group endpoints/models/gen
params, show connection status).
5.**Fix the world/nav mismatch**: `isDetailView` in `App.tsx` only shows
the back button for some views, and `view === "settings"` is treated
separately so a back arrow is missing.
---
## 2. Audit by Surface
### 2.1 Left Rail Navigation — `src/App.tsx`
**Current state (problems):**
- **14 icon buttons in a 56px column** with no labels. Most users will not
hover-discover what each icon does. Two buttons use `Sparkles` (Random
Tables and Quest Designer) — they are visually indistinguishable.
- The rail mixes 7 "primary" nav items, 6 "extra" nav items, and Settings,
with no visual hierarchy. The `flex-1` spacer pushes extras to the bottom
but a new user has no way to know which is which.
- `navItems` is defined in code but `initiative`, `tables`, `quest`,
`calendar`, `lore`, `image` are inlined as separate JSX blocks. The list
is split between the array and 80+ lines of copy-pasted button markup.
- Active-state styling is the same for primary and extras — there's no
indication of "category".
- Tooltips use native `title=""` — slow, ugly, and bad for keyboard users.
- The settings cog duplicates a back-to-dashboard toggle, which is
surprising. Users expect the gear to open settings, not toggle them.
- The order is: World, NPCs, Items, Encounter, Dice, Session, Sound,
Initiative, Tables, Quest, Calendar, Lore, Image, Settings. There is no
grouping by intent (in-session vs prep vs world-building).
**Recommendations:**
- **Two visual groups** with a thin gold separator and a small uppercase
label ("SESSION" / "WORLD") in `--color-text-dim`. Group the 6 most
in-combat tools together (Initiative, Dice, Sound, Encounter, NPCs,
Quest) and the 6 prep/world tools below (World, Lore, Items, Calendar,
Tables, Image).
- **Use distinct icons.** Replace the duplicate `Sparkles` (Tables) with
`Shuffle` or `Dices`. Quest Designer can keep `ScrollText` or get its
own `Flag` icon.
- **Replace native tooltips** with a Radix tooltip or a CSS-only
`data-tooltip` attribute that shows on hover and focus, with a 200ms
delay, 12px `--text-primary` on `--bg-deep` with a 1px gold border.
- **Consolidate the array and inline buttons** into a single
`navItems: { icon, label, view, group }` array, rendered with one
`NavButton` component. Removes ~60 lines of JSX and the bug risk of
drifting styles.
- **Settings as a peer**, not a toggle. Click = always open settings. Use
the title bar's chevron to return to dashboard.
- **Add keyboard shortcuts.** `1``9` jump to the first 9 nav items.
`Cmd+,` opens settings. This is a desktop app — shortcuts are table
stakes (Roll20, Foundry, D&D Beyond all have them).
### 2.2 Title Bar / Header — `src/App.tsx`
**Current state (problems):**
- The header is 40px tall with the app name, a fake "My Campaign ▾"
dropdown that does nothing, and a back button that only shows for some
views (`isDetailView` excludes `settings`).
- `isDetailView` is calculated as `view !== "dashboard" && view !== "settings"`.
That means **on the Settings page there is no back arrow**, so the only
way to leave is the back chevron that doesn't exist there. Users are
stuck — they have to click the dashboard logo in the rail.
- The Tauri drag region is set on the entire header, but the campaign
selector and back button are *inside* the drag region, so they don't
receive click events when the window is dragged from the header (this
is a Tauri gotcha — interactive elements inside a `data-tauri-drag-region`
need `data-tauri-no-drag`).
- "My Campaign" is a placeholder. There's no campaign concept in the
backend yet, but the UI pretends there is one. Either ship the campaign
picker or replace the dropdown with a real status pill (e.g. "● LLM
connected" / "○ offline").
**Recommendations:**
- Make the back button **always show when not on dashboard**, including
Settings. Either drop the `view !== "settings"` clause or invert the
logic to `view === "dashboard" ? null : <BackButton />`.
- Add `data-tauri-no-drag` to the back button and the campaign/status
pill.
- Replace the inert "My Campaign ▾" with a real status indicator:
- LLM connection state (green/gold dot, hover for endpoint)
- Image model state (macOS-gated, "ready"/"unsupported")
- Lore index size ("142 chunks indexed")
- Add a small **global search** `⌘K` that opens a palette to jump to any
tool, NPC, item, or lore source. This is the single biggest navigation
improvement you can ship.
### 2.3 Dashboard / Bento Layout — `src/components/Dashboard.tsx`
**Current state (problems):**
- The dashboard renders **the full component** of every tool inside a
~250×180px tile. The header inside the tile says "World Builder" but the
user sees a tiny 4-character input and a "Generate" button. This is the
core UX problem.
- Every card ends with the same "⤢ Expand" link. So 100% of the time, the
*only* useful interaction is to leave the dashboard. The dashboard is
not a dashboard — it's a launcher.
- The Lore card is a static paragraph ("Index your world bible…") with an
"Open" button, while every other card tries (and fails) to be a real
tool. Be honest about which cards are launchers and which are
at-a-glance summaries.
- The grid uses `grid-cols-1 sm:grid-cols-2 lg:grid-cols-4` with hard
spans. On a 13" laptop (1280px) you get 4 columns, but tiles then
become 280px wide and content is unreadable. There's no 3-column
breakpoint.
- The inline `import`s at the bottom of `Dashboard.tsx` are after the
component — they work due to hoisting, but it's a readability smell.
One import block at the top, sorted.
- World Builder is `col-span-2 row-span-2`. Lore is `1×1`. The "biggest
tile" is also the one with the *least* useful at-a-glance content (a
map you can't interact with at that size). Either give World a real
mini-map (region pin list) or downgrade it to `2×1`.
**Recommendations:**
- **Two dashboard modes**, user-togglable:
1. **Launcher mode (default)** — each card is an icon, title, one-line
description, primary CTA, and a "Last generated: X" footer. No
embedded input. Tap → detail view.
2. **Console mode** — embed 23 chosen tools as real bento cards
(e.g. Dice, Initiative, Sound) for live-session use. User picks
which 3 from a settings list. This is the *real* session screen.
- For the **at-a-glance cards**, show:
- **Dice**: big last result + count of rolls in the session
- **Initiative**: round number + active combatant + # of combatants
- **NPC**: last generated NPC's portrait + name
- **Encounter**: last generated difficulty + monster count
- **Item Forge**: last forged item's name + rarity
- **Session Log**: entry count + last entry timestamp
- **Quest**: current quest title + step X/N
- **Sound**: # of active ambients + master volume
- **Lore**: chunk count + last-added source
- **Image Generator**: model name + last prompt
- **Calendar**: today's fantasy date + next event
- **Tables**: last roll result
- Add a **3-column breakpoint** between tablet and desktop. 4 columns
only on ≥1440px. The bento should be breathable.
- Use **`react-grid-layout`** (already on your roadmap) for draggable,
resizable cards. The plan calls for this — ship it.
- Move all imports to the top of the file.
### 2.4 BentoCard — `src/components/BentoCard.tsx`
**Current state (problems):**
- The card is over-animated: `whileHover={{ scale: 1.005 }}` and
`whileTap={{ scale: 0.985 }}` on every card. With 12 cards on the
dashboard, the whole screen bobs slightly when you move the mouse.
This is the kind of motion that looks great in a portfolio and
terrible in a tool you use for 4 hours.
- The header divider is a `border-b border-[var(--color-border-glass)]`
with no padding below the title. The icon and title sit on the
baseline of the border, so on small cards they crowd the first row
of content.
- The `responsiveSpan` function is fragile: it does
`replace("col-span-2", "sm:col-span-2 lg:col-span-2 col-span-1")` which
means if you pass `col-span-3` it stays as `col-span-3` (no such
class). And `row-span-1` is replaced with itself, so the regex is
busy-work.
- No "drag handle" affordance. If you ship `react-grid-layout`, the
card needs a grab cursor and a small grip icon in the header.
- The body uses `flex-1 overflow-y-auto` which is correct, but nested
scrollbars inside tiny tiles make the bento feel like an ant farm.
**Recommendations:**
- **Drop the whileHover scale.** Replace with a CSS-only border-color
+ box-shadow transition (already in `.glass-card:hover`). Keep
`whileTap` only on buttons.
- Move the `BentoCard` API to a typed prop: `{ title, icon, span, draggable? }`
with a `SPAN_MAP: Record<Span, string>` const. No string regex.
- Add a `headerAction?: ReactNode` slot (for the "Open" / "Expand" button)
so the card's "what to do here" lives in one place.
- For the dashboard launcher mode, the body should be a fixed
`min-h-[120px]` content slot, not `flex-1`. Stop nesting scrollbars.
### 2.5 Detail View Shell — `src/App.tsx`
**Current state (problems):**
- Every detail view is wrapped in `<div className="max-w-2xl mx-auto p-6"><div className="glass-card p-6">…</div></div>`. That means every tool
is **constrained to ~672px wide** in a centered glass card. For a
desktop DM tool, this is too narrow — World Builder, Quest Designer,
Soundboard, and Image Generator all want to be wider.
- The same wrapper is used for every tool, but the tools have very
different needs:
- **Dice**: 400px is plenty
- **World Builder**: 1200px+ (map, side panel)
- **Image Generator**: needs a full preview
- **Soundboard**: a grid that benefits from width
- **LorePanel**: two-pane (add + search) wants 1000px+
- The wrapper has no breadcrumb, no tool-specific header, no actions
(save, copy, share). The back arrow is the only chrome.
- Tools like `LorePanel` and `SessionLogger` already have their own
internal two-pane layout, but they're squeezed into 672px.
**Recommendations:**
- **Drop the universal glass-card wrapper.** Let each tool own its own
page layout.
- Add a **page header** pattern: title (Cinzel, gold) + description +
primary action on the right (e.g. "Save to Lore" / "Regenerate" /
"Copy Markdown"). Reusable `<ToolHeader>` component.
- Use **viewport-width layouts** with sensible max widths per tool:
- Narrow (max-w-2xl): Dice, Settings
- Medium (max-w-4xl): NPC, Item Forge, Encounter, Quest Designer
- Wide (max-w-6xl): World Builder, Lore, Image Generator, Soundboard,
Session Logger
- Each tool should declare its preferred max width via a prop or via
its own root container.
### 2.6 Settings — `src/components/SettingsPanel.tsx`
**Current state (problems):**
- The panel renders an empty state ("Configure your LLM connection")
with a "Load Settings" button. **Settings should always be loaded on
mount.** The "Load Settings" gate is hostile — a user opens Settings
expecting to configure, and instead sees a button they have to click
to *unlock* the form.
- The form is a flat stack of inputs with no grouping, no validation,
no help text, no connection test, and no indication of what changed.
- "API Key" and "Model" inputs are plain text. No "show/hide" toggle on
the API key, no model-name autocomplete, no "Test connection"
button.
- After saving, the "Saved" checkmark lives in the button for 2 s and
disappears. No toast, no persistent indicator.
- Temperature and max_tokens use range sliders with no scale labels
(0 → 2 for temp is meaningless to most DMs). Either show labels
("Focused", "Balanced", "Creative") or numeric input alongside.
- No way to **reset to defaults** or **export/import config**.
- The "embed_model" and "image_model" are tucked at the bottom of the
form with no hint that changing them requires a model download.
**Recommendations:**
- Auto-load on mount, drop the gate.
- **Group settings into sections** with `<fieldset>` and visible legends:
1. **LLM Connection** — API URL, API key, connection test
2. **Text Model** — model, temperature, max tokens, top-p
3. **Image Model** — model, OS-compat note, "Open model library" link
4. **Embedding Model (Lore RAG)** — model, "Reindex all" button
5. **Danger zone** — Reset to defaults, Clear lore index
- **Connection test** button that does a `GET /api/tags` (Ollama) or
`GET /v1/models` (OpenAI) and shows ✅/❌ with the model list.
- **Model picker with autocomplete** that calls `GET /api/tags` and lets
the user select from installed models. Falls back to free text input.
- Show a **persistent status pill** in the title bar driven by the
connection test result.
- Use toasts (you already have `ToastContainer`!) for save success
instead of in-button text.
- Add **presets** for common LLM providers: Ollama local, LM Studio,
OpenAI, Anthropic, Custom. Each preset fills in the API URL pattern.
### 2.7 Dice Roller — `src/components/DiceRoller.tsx`
**Current state (problems):**
- The big result is a single number with no breakdown of which die
rolled what. A 4d6+3 shows `19`, not `[6, 4, 6, 3] +3 = 19`. The
inline history shows the breakdown but only for multi-die rolls.
- No **advantage/disadvantage** buttons — the single most common
d20 roll in 5e. This is table stakes.
- No **modifier input**. The user has to type `1d20+5` every time
even when rolling the same attack repeatedly. There's no concept of
"my character's attack roll" or "my save DC".
- No **3D dice** or animation. The plan calls for `react-three-fiber`
— ship even a simple CSS tumble.
- "Roll" button doesn't react to spacebar. Pressing Enter in the input
does — but most DMs will want spacebar.
- Quick dice buttons only roll `1d{n}` — no `Xd{n}` choice. A "d6"
button rolls 1d6; there's no way to roll 3d6 in one tap.
- History is capped at 50 items with no way to clear or export. No
way to copy a result to clipboard.
- No **roll templates** for common patterns ("Attack", "Save", "Check",
"Damage"). A template would let you pick "Longsword attack +5"
→ rolls 1d20+5 and labels the result.
**Recommendations:**
- Show the **roll breakdown** by default for any roll with >1 die or
any modifier. Format: `[6, 4, 6] + 3 = 19` (the per-die values
always visible inline, with the modifier and total).
- **Advantage/Disadvantage toggle** that swaps the notation to
`2d20kh1` or `2d20kl1` (or rolls twice and picks). Highlight the
chosen die.
- **Modifier input** as a persistent `+/-` field next to the notation
input. Saved per session.
- **Roll template row** with 4 buttons: Attack, Save, Check, Damage.
Each opens a tiny popover with the relevant ability/save/whatever
the DM chooses.
- **Spacebar rolls** the current notation when no input is focused.
- **Quick dice** become `Xd{n}` selectors: tap "d6" once = 1d6, tap
again = 2d6, etc., with a small "×3" indicator.
- History: add **clear** and **copy all** buttons. Show timestamps
in the history.
- Optional: ship a simple **2D dice tumble** with CSS — no 3D
library required. Rotate and scale a die SVG during the roll.
### 2.8 Initiative Tracker — `src/components/InitiativeTracker.tsx`
**Current state (problems):**
- Add row: name, init bonus, max HP, +. **No initiative override**
you can't set a combatant's initiative to a fixed number after the
roll (you have to delete and re-add). Roll20 lets you click the
number and edit.
- Conditions: clicking a `+Bli` chip adds "Blinded". There's no way to
add a **custom condition** ("Concentrating", "Raging",
"Hexed"). For a 5e DM this is critical — almost every combat has at
least one non-standard condition.
- The "Add condition" row shows only 5 conditions at a time. With 13
in the list you have to click through 3 pages to find the right one.
- HP +/- is 1 HP per click. There's no **damage/heal input** — to take
27 damage, you click `-` 27 times. Roll20's tracker has a "Set HP"
input and a "Apply damage" field.
- No **death save** tracking. When HP hits 0, the combatant just
shows 0/HP with no way to roll d20s and track successes/failures.
- No **turn timer**. Combat drags when players stall. A simple
countdown per turn is standard (Foundry, Roll20 both have it).
- "Reset" wipes the whole tracker with no confirmation. Easy to
fat-finger.
- "Next Turn" cycles through combatants but doesn't reveal the
*next* combatant visually. Roll20 and Foundry both bold/highlight
who's up next.
- No **group/encounter import** — you can't say "roll initiative for
the encounter I just built".
- No **notes** per combatant ("Mage is concentrating on Hold Person",
"Bandit leader has 50ft of movement left"). Critical for table
memory.
**Recommendations:**
- **Click initiative number → edit.** Inline number input.
- **Add custom condition**: small "+" button next to the condition
row that opens a popover with text input + auto-suggest from
the 13 standard conditions.
- **Show all 13 conditions** in a small popover instead of the
"slice(0, 5)" loop.
- **HP delta input**: click the HP number → type "27" → press Enter
to take 27 damage. Click the +/- buttons for ±1.
- **Death saves**: when HP=0, show three ✓ and three ✗ toggles.
Roll 1d20 with a button.
- **Turn timer**: optional 60s countdown that turns red at 0. Reset
on "Next Turn".
- **Confirm before Reset** with a small modal.
- **Notes per combatant**: a small `…` menu that opens a textarea
popover.
- **Import from encounter**: button that pulls monsters from the
last generated Encounter into the tracker.
- **Visual highlight of next-up combatant** (gold left-border) so
the DM is primed for "OK, you're up next."
### 2.9 Session Logger — `src/components/SessionLogger.tsx`
**Current state (problems):**
- Two-column layout (notes + AI summary) gets squashed to 672px
in the detail view, making the textarea ~280px wide. Painful.
- Notes are plain text, not Markdown. A "**bold** the BBEG's name" is
shown as asterisks. The plan mentions `@uiw/react-md-editor` — ship it.
- No **session concept**. Every note is in one big unfiltered list.
Where's "Session 4, May 12th"? A DM runs multiple sessions and
needs to switch between them.
- No **timestamp granularity**`toLocaleTimeString()` shows
`3:42:11 PM`. Add the date too.
- No **export**. After a session, the DM needs to share notes with
players, paste into Discord, or save to a Markdown file.
- "AI Summary" is a one-shot button. There's no **streaming**
even though your backend supports it via Channels. You're shipping
the full token list at once. This is a huge UX miss for 520s waits.
- Notes are stored only in component state. **Reload = data loss.**
This is the #1 risk in the app right now.
- No **entry editing** — once added, you can only remove (and the
remove doesn't exist; you can't delete individual entries at all).
- No **tags** or **category** (combat, roleplay, loot, etc.).
**Recommendations:**
- **Persist notes to disk** via `tauri-plugin-store` or `tauri-plugin-fs`.
Load on mount, save on every change (debounced). This is critical.
- **Markdown editor** with a live preview toggle.
- **Sessions** as a first-class concept. Top-bar dropdown lists
sessions; click to switch; "New session" button; archive old ones.
- **Streaming AI summary** using your existing Channel infrastructure.
Show tokens as they arrive, with a gold cursor.
- **Export** to Markdown file (Save As) and Copy to Clipboard.
- **Per-entry actions**: edit, delete, mark as "highlight" (gold
left border).
- **Tags** with auto-suggest: #combat #roleplay #loot #quest.
- **Timestamps** include date.
### 2.10 Encounter Builder — `src/components/EncounterBuilder.tsx`
**Current state (problems):**
- Generates a *narrative* encounter, not a *balanced* one. There's no
CR calculation, no XP budget check, no party-vs-encounter
difficulty math. Kobold Fight Club and the DMG have well-known
formulas. DMs will expect this from a tool called "Encounter
Builder."
- Monsters are listed as strings ("3x Goblin Scouts") with no
stat block. The DM has to flip to the MM. Pull from SRD or your
own DB.
- "Difficulty" is a string the LLM generated — "Easy" or
"Hard" or sometimes "Moderate" (not in your enum). Normalize and
color-code.
- No **initiative setup**. After generating, you can't push the
monsters straight into the Initiative Tracker.
- No **save** — generated encounters vanish on refresh.
- "Terrain" select only has 8 options and is free-form from the
LLM's side. No "tavern brawl", "ship deck", "fey crossing"
presets.
**Recommendations:**
- **Add XP budget math.** Inputs: party size, party level (already
have both), desired difficulty (Easy/Medium/Hard/Deadly). Output
table from DMG p.82. Show budget and current XP live.
- **Stat block integration.** Either inline (parse from a local
monsters.json) or a "View stat block" popover with SRD content.
- **Color-coded difficulty** pill (green / blue / orange / red).
- **"Send to Initiative Tracker"** button. Builds the combatant list
with monsters' CR-derived max HP.
- **"Send to Soundboard"** button — picks ambient based on terrain
(forest → 🌲 Forest ambient).
- **Save to campaign** (long-term) or **save to lore** (so future
encounters reference this one).
- **Terrain presets** with icons: 🏰 dungeon, 🌲 forest, ⛰ mountain,
🏛 urban, 🌊 coastal, 🕳 underdark, 🏜 desert, 🌫 swamp. Add 4 more:
✈ planar, 🏚 haunted, 🚢 ship, 🏰 siege.
### 2.11 NPC Generator — `src/components/NpcGenerator.tsx`
**Current state (problems):**
- "Class" is a free-form text input labeled "Class" but it accepts
any string — "Artisan", "Blacksmith", "Diplomat", "Pickpocket".
These are *backgrounds*, not classes. The 5e concept of "class"
(Fighter, Wizard) is not enforced. Either rename the field to
"Background/Occupation" or show a class dropdown alongside.
- 8 races, 9 alignments — these are fine but a DM who wants a
half-orc or a goliath can't. Expand to the SRD 9 races.
- Portrait is a *single* image. No way to view full-size. No
"save portrait" button.
- Personality and goals are flat lists. No way to **edit** them
after generation.
- No **save NPC to roster**. Generated NPCs vanish on refresh.
- No **NPC card** concept — the result is a one-off. A DM accumulates
dozens of NPCs and needs a roster.
- The "🎲 random name" placeholder in the Name field is just a
placeholder — there's no actual "randomize name" button.
- No **stats**. 5e NPCs have AC, HP, ability scores, saves, skills.
The generator produces flavor, not a usable stat block.
**Recommendations:**
- Rename "Class" to "Background" with a curated list (Acolyte,
Criminal, Folk Hero, Noble, Sage, Soldier, plus 13 more from PHB
backgrounds). Add a free-text "Class" dropdown for Fighter/Wizard/…
separately.
- Expand race list to 9 (add Half-Orc, Goliath) and add a
"Subrace" optional dropdown.
- **NPC Roster** as a new view: grid of all generated NPCs with
portrait, name, race. Click to open full sheet.
- **Stat block** section: AC, HP, ability scores, saves. Either
generated by LLM or hand-entered.
- **Random name** button with a dice icon.
- **Save portrait** (right-click → save image).
- **Edit personality/goals** in-place.
- **Copy as Markdown** for the DM's notes.
### 2.12 Item Forge — `src/components/ItemForge.tsx`
**Current state (problems):**
- Rarity is colored, but the colors are 6 hard-coded hex values.
If a future rarity is added, the color falls back to gold. Use a
rarity→color map.
- "Type" is a free-form text input labeled "Type" but the dropdown
is "Weapon", "Armor", "Potion", "Scroll", "Wondrous Item",
"Ring", "Wand", "Staff" — that's 8 of 12 standard magic item
types. Missing: Rod, Staff ✓, Wondrous ✓, Adventuring Gear,
Tacked.
- Art is a single 1024×1024 in a 96px frame. The DM can never see
the full image without exporting.
- "mechanical" is a string. 5e items have *attunement*, *rarity
tag*, *value*, *weight*. The LLM prompt asks for a single
paragraph — you get a paragraph.
- No **save to inventory**. Items vanish.
- No **random item** button (one-click "surprise me").
**Recommendations:**
- **Click art to expand** to full-size in a modal.
- **Structured mechanics** field: attunement (yes/no/condition),
rarity tag (already have), charges, value, weight, single-line
effect description. LLM returns these as a small object.
- **Save to inventory** → list view of all forged items with
rarity-coded borders. Click to view full sheet.
- **Random item** dice button.
- **Export to Markdown** / copy to clipboard.
### 2.13 World Builder — `src/components/WorldBuilder.tsx`
**Current state (problems):**
- **Critical syntax bug**: lines 132, 147, 161 have `})` instead of `}`
(extra `)`). The component renders but with broken JSX. Confirmed
by reading the file. *Fix this first.*
- Generates text (name, description, regions, landmarks) but **no
map**. The plan calls for `react-konva` map rendering. The 2×2
dashboard tile is wasted on a text blob.
- "Theme" is a single text input. No presets (high fantasy, dark
fantasy, sword & sorcery, steampunk, post-apocalyptic, sci-fi,
horror).
- "Regions" is a flat list. No **hierarchy** (continent → country
→ region → city) and no **map pins**.
- No **save world**. Generated worlds vanish.
- No **edit**. Generated content is read-only.
- The Dashboard tile embeds the *full* WorldBuilder component, so
you see a 280px text input that says "high fantasy" with a
"Generate" button. Unusable at that size.
**Recommendations:**
- **Fix the syntax bug** in `WorldBuilder.tsx` (lines 132, 147, 161).
- **Map canvas**: `react-konva` or `fabric.js` with a parchment
background, region pin overlays, click-to-edit. This is the
"wow" feature of the app — invest here.
- **Theme presets** as a 4×2 grid of clickable chips.
- **Hierarchy tree**: collapsible tree of continent → country →
region → city, on the left. Each node has its own generated
detail.
- **Persist** the world to disk, reload on mount.
### 2.14 Random Tables — `src/components/RandomTables.tsx`
**Current state (problems):**
- Only 4 built-in tables. DMs want hundreds. The 5e DMG alone has
~30. Add: NPC Names (fantasy, by race), Traps, Dreams, Treasure
Hoards, Dungeon Encounters, Wilderness Encounters, Urban
Encounters, Caravan Cargo, Inn Names, Ship Names, etc.
- No **user-defined tables**. DMs will want to roll on their own
table of "weird things found in the dungeon."
- No **table import** (paste a table from a PDF/book).
- "Roll 1d20" button always uses the table's dice — no override.
- History is capped at 30, with no clear, no copy.
- Result is shown in the table preview (highlighted gold row) AND
in the big number box, which is redundant.
- No **weighted tables** — all entries are `minmax` ranges. Some
tables should weight "1: 10%, 2: 30%, 3: 60%".
**Recommendations:**
- **Add 10 more built-in tables** (Traps, Dreams, NPC Names, etc.)
or ship a community table pack.
- **User-defined tables**: create / edit / delete. List view.
- **Table import**: paste a markdown list, parse to entries.
- **Weighted tables**: when entries don't cover 100% of the dice
range, show the weights visually.
- **Result**: keep just the big number, highlight the row in the
table, no separate "result" block.
- **Export table** to JSON / Markdown.
### 2.15 Calendar — `src/components/CalendarWidget.tsx`
**Current state (problems):**
- Uses the Forgotten Realms calendar (Hammer, Alturiak, …) but
there's no way to **switch to a custom calendar**. Half the
campaigns are homebrew with different month names.
- "Today" is hard-coded to `day: 15, month: 5`. The DM has to
manually navigate to "today."
- No **weather** on calendar days (the plan calls for weather
generation — not implemented).
- No **moon phases** (essential for lycanthropes, druids).
- Events are color-cycled through 4 colors. No way to assign a
color or category.
- No **recurring events** ("The Festival of Masks happens every
year on 12 Eleint").
- The day grid shows 30 days for every month. The Forgotten
Realms calendar has 30-day months, but a custom calendar might
not.
- No **agenda view** ("next 5 events") — useful at session start.
**Recommendations:**
- **Custom calendar editor**: months, days per month, year length,
weekday names. Save as part of the campaign.
- **Current campaign date** as a "Today" indicator the DM can
advance day-by-day with a "Next day" button.
- **Weather generator** that rolls based on season/terrain, shown
as an icon on the day.
- **Moon phase** per day (8 phases cycling).
- **Event categories** with colors: Festival, Quest, NPC Birthday,
Political, Custom.
- **Recurring events** (yearly/monthly).
- **Agenda view** as an alternate layout.
### 2.16 Soundboard — `src/components/Soundboard.tsx`
**Current state (problems):**
- All sounds are **synthesized** with Web Audio. Rain, fire, ocean
are not bad, but a DM running a session wants *real* recordings:
actual rain, actual crackling fire, actual ambient music. The
synthesis is clever but sounds thin next to a real ambience pack.
- No way to **import custom sounds** (drag an MP3 into a tile).
- No **fade-out** when stopping an ambient — only a 0.5s ramp.
Sometimes you want a 5s crossfade to a new ambient.
- No **scenes** ("Tavern" = tavern ambient + chatter + dice SFX +
fire; "Combat" = wind + drums + sword). One-click scene
switching.
- No **master mute** for the whole board.
- The "Stop All" button is below the SFX grid, easy to miss.
- No way to **save the current setup** ("This is what I use for
the dungeon delve").
- No way to **loop count** for one-shots (e.g. dice rattle twice
for a crit).
- `AudioContext` is created on first play, but the browser
autoplay policy can block it until a user gesture. The first
ambient may not start. Use `await ctx.resume()` if suspended.
**Recommendations:**
- **Real sound packs**: ship 23 royalty-free ambience loops
(Pixabay, Freesound.org CC0). Synthesized stays as fallback.
- **Custom sound import**: drag an MP3/WAV onto a tile to assign.
- **Scenes**: a named collection of "on" sounds with per-scene
volume. "Tavern" / "Combat" / "Dungeon" / "Wilderness" / Custom.
- **Master mute** and **per-sound volume** (currently global
volume only).
- **Always-visible Stop All** as a fixed footer button, not
conditionally rendered.
- **Save/load board state**.
- **Pre-warm AudioContext** on first user gesture (any click).
### 2.17 Quest Designer — `src/components/QuestDesigner.tsx`
**Current state (problems):**
- Quests are linear steps in a carousel. DMs want **branching**
outcomes ("If they spare the bandit, go to step 3; if they kill
him, go to step 5"). The plan calls for `react-flow` — ship it.
- No **save quest**. Generated quests vanish.
- No **player-facing view** (a quest the DM shows to players
sanitized of twists and secret goals). D&D Beyond has this.
- No **status** on each step (Pending / Active / Done). The
stepper is just a viewer.
- No **reward distribution** — rewards are flavor text, not
split-by-character XP/gold/items.
- "Theme" is one text field. No presets.
- No **quests list / roster**.
**Recommendations:**
- **Branching quest graph** with `react-flow`. Gold edges for main
path, blue for side branches, red for "if villain wins."
- **Step status** (Pending / Active / Done) with click-to-advance.
- **Player view** toggle: hides Twist, shows flavor only.
- **Reward breakdown**: XP (per character), gold, items.
- **Quest roster**: list of all generated quests with status
badges.
- **Save to lore** so generated quests feed back into the world.
### 2.18 Image Generator — `src/components/ImageGenerator.tsx`
**Current state (problems):**
- Prompt textarea is one big block. No **negative prompt** field
(no "blurry, low quality, watermark").
- No **seed** field for reproducibility.
- No **aspect ratio selector** — always 1024×1024. A DM wants
16:9 for a battle map, 1:1 for a portrait, 9:16 for a phone
wallpaper.
- No **steps / guidance scale** controls.
- No **batch** (generate 4 variants, pick the best).
- No **gallery** — all generated images are ephemeral. DMs will
want a folder of "this campaign's portraits."
- "Save PNG" downloads a single image. No "save all", no
"copy to clipboard."
- "Regenerate" only fires one variant. No seed/parameter changes.
- The unsupported-platform error is shown but the ✨ buttons
elsewhere (NPC, Item) silently fall back. Inconsistent.
**Recommendations:**
- **Negative prompt** + **seed** + **aspect ratio** + **steps** +
**guidance** controls. Collapsible "Advanced" section.
- **Batch mode**: 2×2 grid of variants, click the one you like.
- **Gallery**: thumbnails of all generated images in a grid.
Click to view full size. "Save all" / "Delete".
- **Persist** to a campaign-scoped folder.
- **Copy to clipboard** in addition to download.
- **Consistent macOS gating**: when unsupported, the ✨ buttons
in NPC/Item should also show a "macOS only" inline notice, not
silent fallback.
### 2.19 Lore Panel (RAG) — `src/components/LorePanel.tsx`
**Current state (problems):**
- Three sections stacked vertically: Add, Indexed, Test. With a
672px-wide detail view, the textarea for "paste your world bible"
is unusable.
- No **drag-and-drop file upload** (.md, .txt, .pdf).
- No **directory import** ("add every .md in /Users/me/dnd/world").
- No **chunk preview** (what text was actually chunked and
embedded?).
- No **embedding visualization** (t-SNE or UMAP of the corpus).
- Search results show score with 3 decimal places (0.873). Most
DMs don't know what that means. Show "★ 4/5" or a relevance
bar.
- No **search history**.
- No **way to attach lore to a generation** — the `ragQuery`
field on the LLM command exists, but the user has no UI to
pick which lore sources to query against.
- "Clear all" is a one-click nuke. Confirm first.
**Recommendations:**
- **Two-pane layout**: left = add/import, right = search/explore.
- **Drag-and-drop file upload** with file-type validation.
- **Directory picker** via Tauri dialog.
- **Chunk preview** (first 200 chars of each chunk) when a source
is expanded.
- **Source filter** in the search input — multi-select chips.
- **Visualize as "★ relevance"** not raw cosine distance.
- **Search history**.
- **Confirm before "Clear all"** with a modal.
### 2.20 GeneratedImage — `src/components/GeneratedImage.tsx`
**Current state (problems):**
- Used in NPC and Item Forge. The component fetches on every mount
*and* every `nonce` change. If the parent re-renders for any
reason (state update, etc.), the nonce is unchanged so the
same image is fetched — fine. But the **prompt** changes when
the NPC changes, and there's no `AbortController` to cancel an
in-flight image gen when the user changes inputs mid-flight.
- No **progress** for image generation (the backend streams
`step`/`total` lines). Show a progress bar.
- No **placeholder** while loading — just a tiny `Sparkles`
icon, which is invisible in a 96px tile. Show a skeleton with
a shimmer.
- On error (not macOS), shows a "macOS-only" placeholder. The
message is small and grey. A DM on Windows might not realize
*why* nothing is happening.
- On regular error, shows the error string inline. Could be a
10-line stack trace.
**Recommendations:**
- **AbortController** in the useEffect cleanup so stale requests
don't overwrite the latest.
- **Progress bar** driven by the NDJSON `step`/`total` lines from
the backend. Pass through a `Channel`.
- **Skeleton with shimmer** during load.
- **Friendly error messages** for known error codes.
### 2.21 Toast — `src/components/Toast.tsx`
**Current state (problems):**
- The component is solid. But: only `Soundboard` uses it. Every
other tool uses inline error boxes that duplicate the toast's
job. **Use toasts everywhere** — they're a unified feedback
channel.
- Auto-dismiss is 4s with no way to extend. Power users may want
sticky toasts.
- The 4 toast types have gold-tinted styles, but only `info`
uses gold. Make `success` green-tinted to be more
distinguishable.
- The `nextId` module-level counter is shared across imports.
Fine in dev, brittle in tests. Use a UUID or `crypto.randomUUID()`.
- Stacking: 5 toasts push each other off-screen. Cap at 3 visible
at once.
**Recommendations:**
- **Standardize on toasts** for save success, generate complete,
generation error, etc. Delete inline error boxes from
NPC/Encounter/Quest/etc.
- **Sticky option**: `addToast(msg, type, { sticky: true })`.
- **Cap visible** at 3; older ones auto-dismiss sooner.
- **UUIDs** for IDs.
### 2.22 Cross-Cutting Issues
#### Accessibility
- **No keyboard nav** between nav items, or only the default
tab order. The left rail buttons are focusable but there's no
visible focus ring style (the global `*:focus-visible` is set
to a 2px gold outline, but it gets clipped on the 14px-wide
rail buttons).
- **No aria-labels** on icon-only buttons. Screen readers
announce "button" with no name.
- **Color-only state** in Initiative (HP bar green→red) and Item
Forge (rarity colors). Add a label or icon.
- **No skip-to-content** link. Tab through the rail to get to
the dashboard.
- **Modal dialogs** (when added) need focus traps.
- **Text contrast**: `--color-text-dim` (#4a5568) on `--color-bg-deep`
(#0a0e1a) is ~3.2:1 — below WCAG AA (4.5:1 for small text).
Lighten it.
- **No reduced-motion support**. `framer-motion` animations should
respect `prefers-reduced-motion`.
#### Internationalization / i18n
- All strings are hard-coded English. If you ever ship to non-EN
markets, you'll need a copy pass. Not a v1 blocker.
#### Offline-First
- Fonts are loaded from Google Fonts in `index.css` — the
comments admit "TODO: download woff2 files to public/fonts/
for offline-first." This is a desktop app meant to run offline.
Ship the fonts locally.
#### Error Boundaries
- **No React error boundaries**. If the LLM returns malformed
JSON that crashes a parser, the whole app blanks to a white
screen. Add an `<ErrorBoundary>` around the main content area
with a "Reload" button.
#### Loading Skeletons
- No skeleton states anywhere. Every async tool shows a static
"Generating…" button or a tiny spinner. Add skeleton cards for
the bento, skeletons for AI generations (shimmer placeholders
that match the final layout).
#### Empty States
- The Lore panel has one ("No lore indexed yet"). The Initiative
tracker has none (empty list looks broken). The Encounter
builder has none. The Quest designer has none. **Every tool
needs an empty state** with a 1-line explanation and a primary
action.
#### Keyboard Shortcuts
- `⌘K` command palette (mentioned above)
- `1``9` jump to nav items
- `Space` rolls dice
- `⌘,` opens settings
- `⌘S` saves current tool's state
- `Esc` closes modals / goes back to dashboard
- `?` shows shortcut help
#### Persistence
- **Almost nothing persists.** Generated NPCs, encounters,
items, quests, dice history, initiative, calendar events —
all component state. **Reload = data loss.** This is the
single biggest functional gap.
- Use `tauri-plugin-store` for user preferences and small
lists.
- Use `tauri-plugin-fs` + JSON files in `$APPDATA/dm-pal/`
for generated content.
- Auto-save on every change (debounced).
#### Dark Mode
- App is dark-only (intentional). But the macOS light title bar
is jarring. Set `titleBarStyle: "Overlay"` or
`Transparent` in `tauri.conf.json`.
---
## 3. DM-Specific UX Notes (External Research)
Sources consulted: Roll20 Turn Tracker docs, Roll20 VTT Redesign
research post (Brittany Vick, 2023), Roll20 D&D 5e Character
Builder case study, D&D Beyond product walkthroughs (general
familiarity).
### 3.1 Roll20's design lessons that apply
- **"Less Menus, More Suggestions."** DM-Pal's bento is menu-
heavy. Surface suggestions: "Roll initiative for this encounter"
one-click button on a generated encounter. "Add this NPC to
the lore" after generation.
- **"Simplification."** Reduce repetitive actions. Today, after
generating an encounter, the DM has to: copy monster names →
open initiative tracker → re-type names → set HP → roll
initiative. Auto-push instead.
- **"Automation."** Auto-save, auto-import, auto-reroll.
Encounter → Initiative push. Item → Inventory push. NPC → Roster
push. Quest → Lore push.
- **"Player view vs GM view."** DM-Pal is GM-only, but the
concept of "what does the DM see vs. what's projected" still
applies. A session-view mode (large Initiative, large Dice,
large Soundboard, no edit chrome) for when the DM is sharing
the screen.
### 3.2 Combat tracker patterns (Roll20, Foundry, D&D Beyond)
- Foundry: HP and AC always visible; conditions as small icons.
- D&D Beyond: turn order vertical; "your turn" glow; HP and
AC prominent.
- Roll20: drag-and-drop reordering; round calculation for
custom items ("-1 per round" simulates a countdown).
- **All three** show: round number prominently, current actor
highlighted, next actor visible.
- **None** force you to click +/- 27 times for damage. All have
a numeric input.
### 3.3 Image generation in DM tools
- **Foundry** has no AI image gen (yet).
- **D&D Beyond** has AI art via the partner artist program.
- **NovelAI**, **Stable Diffusion WebUI** are the open-source
references — they all have: negative prompt, seed, aspect
ratio, batch, gallery. **Ship these in DM-Pal's image gen.**
### 3.4 Random tables reference
- **Kobold Press**, **Donjon**, **Seventh Sphere** all have
online random tables. **Donjon's** UI is the gold standard:
big "Roll" button, immediate result, table visible below with
the result highlighted. DM-Pal's RandomTables is close — just
needs more tables and user-defined tables.
### 3.5 Calendar reference
- **Fantasy Calendar** (5e calendar tool) is the de-facto
standard. Features: custom calendars, recurring events, moon
phases, weather, agenda view, export to ICS. DM-Pal's calendar
is a sketch — there's a 10× feature gap.
### 3.6 Soundboard reference
- **Syrinscape**, **Tabletop Audio** are the reference points.
Both ship **real recordings** (synthesized doesn't cut it for
immersion). Both have **scenes** (one-click combinations).
DM-Pal's synthesis is clever but a real ambience pack is the
obvious upgrade.
---
## 4. Prioritized Checklist
### P0 — Critical (fix this week)
These are blockers or bugs that make the app feel broken.
- [x] **Fix `WorldBuilder.tsx` syntax bug** (lines 132, 147, 161:
`})` should be `}`). Currently renders broken JSX.
- [x] **Auto-load Settings on mount.** Remove the "Load Settings"
gate.
- [x] **Add `data-tauri-no-drag` to interactive elements in the
header** (back button, status pill).
- [x] **Show back button on Settings** (drop the
`view !== "settings"` exclusion in `isDetailView`).
- [x] **Add a React error boundary** around the main content
area.
- [x] **Self-host the fonts** in `public/fonts/` (offline-first).
- [x] **Standardize errors via the Toast system** — delete inline
error boxes from NPC, Encounter, Quest, World, Item, Image.
- [x] **Replace native `title=""` tooltips** with a styled
`data-tooltip` (visible on hover and focus).
- [x] **Add `aria-label`** to every icon-only button.
### P1 — High impact (next sprint)
- [x] **Reorganize the left rail** into 2 visual groups
(Session / World) with a thin gold separator and small
uppercase labels.
- [x] **Replace the duplicate `Sparkles` icon** on Random Tables
(use `Shuffle` or `Dices`).
- [x] **Add a global ⌘K command palette** for navigation.
- [x] **Implement persistence** for Initiative, Dice history,
Calendar events, Generated NPCs/Encounters/Items/Quests.
Use `tauri-plugin-store` for small state, `tauri-plugin-fs`
for generated content.
- [x] **Ship a loading skeleton + streaming UI** for LLM and
image generation. Use the existing Tauri Channel
infrastructure.
- [x] **Add roll breakdown** to Dice Roller for any roll with
>1 die or any modifier.
- [x] **Add Advantage/Disadvantage buttons** to Dice Roller.
- [x] **Make the dashboard a launcher** — replace embedded
mini-tools with at-a-glance summaries + primary CTA.
- [x] **Drop the universal `max-w-2xl glass-card` wrapper** in
the detail view. Let each tool set its own max-width.
- [x] **Add a real loading state** to Settings: connection test
with model list.
- [x] **Replace "My Campaign" placeholder** with a real status
pill (LLM connection, image model, lore chunk count).
- [x] **Lighten `--color-text-dim`** to meet WCAG AA (4.5:1)
against `--color-bg-deep`.
### P2 — Important (next month)
- [x] **Initiative Tracker**:
- [x] Click-to-edit initiative number
- [x] Custom conditions
- [x] Damage input (numeric, not +/- only)
- [x] Death saves
- [x] Turn timer
- [x] Notes per combatant
- [x] Highlight next-up combatant
- [x] Confirm before Reset
- [x] "Import from Encounter" button
- [x] **Dice Roller**:
- [x] Modifier input (saved per session)
- [x] Spacebar rolls
- [x] Roll templates (Attack, Save, Check, Damage)
- [x] Clear/copy history
- [x] **Encounter Builder**:
- [x] XP budget math (party size, level, difficulty)
- [x] Color-coded difficulty
- [x] Send to Initiative Tracker
- [x] Send to Soundboard (terrain-based)
- [x] Save to lore
- [x] 4 more terrain presets (planar, haunted, ship, siege)
- [ ] **NPC Generator**:
- [x] Rename "Class" to "Background" with curated list
- [x] Expand races to 9 (Half-Orc, Goliath)
- [ ] NPC Roster view (History view covers re-opening past NPCs)
- [x] Stat block section (AC, HP, ability scores)
- [x] Random name button
- [ ] Save portrait (right-click)
- [x] Edit personality/goals in-place
- [ ] **Item Forge**:
- [x] Click art to expand
- [x] Structured mechanics (attunement, charges, value, weight)
- [ ] Save to inventory (History view covers re-opening past items)
- [x] Random item button
- [ ] **Quest Designer**:
- [ ] Branching quest graph (react-flow)
- [x] Step status (Pending/Active/Done)
- [x] Player-facing view toggle (hide Twist)
- [x] Reward breakdown (XP per character, gold, items)
- [ ] Quest roster (History view covers re-opening past quests)
- [ ] **Image Generator**:
- [ ] Negative prompt, seed, aspect ratio, steps, guidance
- [x] Batch mode (2×2 variants)
- [x] Gallery view with thumbnails
- [x] Persist to campaign-scoped folder
- [x] Copy to clipboard
- [x] **Session Logger**:
- [x] Markdown editor + live preview (minimal renderer; no dep)
- [x] Sessions as first-class concept (multiple sessions)
- [x] Streaming AI summary
- [x] Export to Markdown
- [x] Per-entry actions (edit, delete, highlight)
- [x] Tags
- [x] **Calendar**:
- [x] Custom calendar editor
- [x] Current campaign date with "Next day" button
- [x] Weather generator
- [x] Moon phases
- [x] Event categories with colors
- [x] Recurring events
- [x] Agenda view
- [ ] **Soundboard**:
- [ ] Real ambience pack (23 royalty-free loops)
- [ ] Custom sound import (drag-drop)
- [x] Scenes (one-click combinations)
- [x] Master mute + per-sound volume
- [x] Always-visible Stop All
- [x] Save/load board state
- [x] Pre-warm AudioContext
- [x] **Random Tables**:
- [x] 10+ more built-in tables
- [x] User-defined tables (create/edit/delete)
- [x] Table import (paste Markdown)
- [x] Weighted tables (weight % + gap warning)
- [x] Export table
- [ ] **World Builder**:
- [x] Map canvas (react-konva)
- [x] Theme presets
- [ ] Hierarchy tree
- [x] Persist world
- [x] **Lore Panel**:
- [x] Two-pane layout
- [x] Drag-drop file upload (via native file input; directory picker needs tauri-plugin-dialog)
- [ ] Directory picker (needs tauri-plugin-dialog)
- [x] Chunk preview
- [x] Source filter
- [x] ★ relevance (not raw cosine)
- [x] Confirm before "Clear all"
- [x] **Settings**:
- [x] Group into fieldsets (Connection, Text, Image, Embedding, Danger)
- [x] Connection test with model list
- [x] Model picker with autocomplete (`GET /api/tags`)
- [x] Provider presets (Ollama, LM Studio, OpenAI, Anthropic, Custom)
- [x] Reset to defaults
- [x] Export/import config
- [x] Configurable campaign data location (lore db, generations db, generated images) via a folder picker — migrates existing data and applies on restart
- [x] **Cross-cutting**:
- [x] Keyboard shortcuts: ⌘K, 19, Space, ⌘,, ⌘S, Esc, ?
- [x] Skeleton loaders for all async states
- [x] Empty states for all tools
- [x] `prefers-reduced-motion` respect on framer-motion
- [x] Skip-to-content link
- [x] Color + label for state (not color alone)
### P3 — Nice to have
- [ ] **Draggable bento layout** (react-grid-layout) with
persisted layout per campaign.
- [ ] **Console mode** for the dashboard — embed 23 chosen
tools as live-session cards.
- [ ] **Tablet/phone player view** — a web app that shows the
DM's screen (initiative, dice) to players on their
phones via the local network.
- [ ] **Compendium integration** — pull monster stat blocks from
a local SRD JSON.
- [ ] **i18n** — extract strings to a t() helper.
- [ ] **Session replay** — record dice rolls, initiative
changes, soundboard state, and replay the session.
- [ ] **Macros** — let DMs define "1d20+5 attack roll" as a
named button on the dice roller.
- [ ] **Voice-to-text** for session notes (Whisper).
---
## 5. Quick-Win Sprint (suggested first PR)
If you want a single PR that lands a lot of value, here are 8
items that take <1 day combined and touch mostly `App.tsx` and
small CSS. **All 10 below are now shipped.**
1. Fix the `WorldBuilder.tsx` syntax bug (5 min)
2. Auto-load Settings on mount (10 min)
3. Drop the `view !== "settings"` exclusion on the back button
(2 min)
4. Add `aria-label` to every icon-only button (30 min)
5. Add `data-tooltip` styled tooltips (1 hr)
6. ✓ Group the left rail into Session / World (30 min)
7. ✓ Replace the duplicate `Sparkles` icon (2 min)
8. ✓ Lighten `--color-text-dim` to meet WCAG AA (2 min)
9. ✓ Add the ⌘K command palette (3 hrs, the biggest of the bunch)
10. ✓ Self-host the fonts in `public/fonts/` (1 hr)
Total: ~68 hours. Lands a much more polished, accessible
baseline that the rest of the checklist builds on.
---
## 6. References
- [Roll20 Turn Tracker docs](https://help.roll20.net/hc/en-us/articles/360039178634-Turn-Tracker)
- [Roll20 VTT Redesign research (2023)](https://blog.roll20.net/posts/roll20-virtual-tabletop-redesign-our-research/) — Brittany Vick, UX Lead
- [Roll20 Design validation process (2024)](https://blog.roll20.net/posts/validating-digital-ttrpg-designs-at-roll20/)
- [Roll20 InitiativeTrackerPlus script](https://wiki.roll20.net/Script:InitiativeTrackerPlus) — community patterns
- [Tome of Tips: Turn Tracker](https://bloghub.roll20.net/posts/tome-of-tips-turn-tracker/) — round calculation patterns
- [WCAG 2.1 contrast minimums](https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html) — 4.5:1 for small text
- [DMG encounter math (p.82)](https://www.dndbeyond.com/sources/basic-rules/building-combat-encounters) — XP thresholds by level
- [D&D 5e SRD races](https://dnd.wizards.com/resources/systems-reference-document) — 9 races (Human, Elf, Dwarf, Halfling, Gnome, Dragonborn, Tiefling, Half-Orc, Goliath)
- [Tauri v2 IPC and Channels](https://v2.tauri.app/develop/calling-rust/)
- Internal: `docs/plan.md` — original feature plan and milestones