# 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 largely done. 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 5–20 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 2–3 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 5–20s 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 `min–max` 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 2–3 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)

- [ ] **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
- [ ] **Dice Roller**:
  - [x] Modifier input (saved per session)
  - [x] Spacebar rolls
  - [x] Roll templates (Attack, Save, Check, Damage)
  - [x] Clear/copy history
- [ ] **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
  - [ ] 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
  - [ ] Structured mechanics (attunement, charges, value, weight)
  - [ ] Save to inventory
  - [x] Random item button
- [ ] **Quest Designer**:
  - [ ] Branching quest graph (react-flow)
  - [x] Step status (Pending/Active/Done)
  - [x] Player-facing view toggle (hide Twist)
  - [ ] Reward breakdown (XP per character, gold, items)
  - [ ] Quest roster
- [ ] **Image Generator**:
  - [ ] Negative prompt, seed, aspect ratio, steps, guidance
  - [ ] Batch mode (2×2 variants)
  - [ ] Gallery view with thumbnails
  - [x] Persist to campaign-scoped folder
  - [x] Copy to clipboard
- [ ] **Session Logger**:
  - [ ] Markdown editor + live preview
  - [ ] Sessions as first-class concept (multiple sessions)
  - [x] Streaming AI summary
  - [x] Export to Markdown
  - [x] Per-entry actions (edit, delete, highlight)
  - [ ] Tags
- [ ] **Calendar**:
  - [ ] Custom calendar editor
  - [x] Current campaign date with "Next day" button
  - [x] Weather generator
  - [x] Moon phases
  - [ ] Event categories with colors
  - [ ] Recurring events
  - [ ] Agenda view
- [ ] **Soundboard**:
  - [ ] Real ambience pack (2–3 royalty-free loops)
  - [ ] Custom sound import (drag-drop)
  - [ ] Scenes (one-click combinations)
  - [x] Master mute + per-sound volume
  - [x] Always-visible Stop All
  - [x] Save/load board state
  - [x] Pre-warm AudioContext
- [ ] **Random Tables**:
  - [x] 10+ more built-in tables
  - [ ] User-defined tables (create/edit/delete)
  - [ ] Table import (paste Markdown)
  - [ ] Weighted tables
  - [x] Export table
- [ ] **World Builder**:
  - [x] Map canvas (react-konva)
  - [x] Theme presets
  - [ ] Hierarchy tree
  - [x] Persist world
- [ ] **Lore Panel**:
  - [ ] Two-pane layout
  - [ ] Drag-drop file upload
  - [ ] Directory picker
  - [ ] Chunk preview
  - [ ] Source filter
  - [x] ★ relevance (not raw cosine)
  - [x] Confirm before "Clear all"
- [ ] **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
  - [ ] Export/import config
- [ ] **Cross-cutting**:
  - [x] Keyboard shortcuts: ⌘K, 1–9, 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 2–3 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: ~6–8 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
