# 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 : `. - 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` 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 `
`. 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 `` 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 `
` 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 `` 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**: - [ ] 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