# 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 `