# Feature Landscape — v0.2 UI Overhaul

**Domain:** Self-hosted SaaS document management (DocuVault)
**Researched:** 2026-06-07
**Scope:** UX patterns and UI feature decisions for v0.2. Existing functional features (upload, extraction, sharing, cloud backends, auth) are not re-researched — only their UX surface.

---

## Table Stakes

Features users expect. Missing = product feels incomplete or broken.

| Feature | Why Expected | Complexity | Depends On |
|---------|--------------|------------|------------|
| Standalone admin layout (`/admin/*` routes with own sidebar) | AdminView.vue is currently a tab sheet bolted to the user layout. Admin and user contexts share no chrome. Nextcloud, Mayan EDMS, and every self-hosted SaaS treat admin as a distinct area. | Medium | Router nested routes, new `AdminLayout.vue` |
| Admin sidebar with discrete nav items (Users, Quotas, AI Config, Audit Log) | Each admin tab is already a full standalone component. Route-based nav replaces in-view tabs — enables deep linking, browser back button, and future per-section permissions. | Low | Vue Router `/admin/users`, `/admin/quotas`, `/admin/ai`, `/admin/audit` child routes |
| Empty states for every zero-content context | File list empty, folder empty, search no-results, shared-with-me empty, topics empty — each needs a distinct, actionable state. Current implementation is plain gray text. Zero-content states are the first thing new users see. | Medium | `StorageBrowser.vue`, `SharedView.vue`, `TopicsView.vue` |
| Loading skeletons for document list and sidebar tree | The document list (`StorageBrowser`) and sidebar folder tree currently render a plain "Loading…" text string. Skeletons reduce perceived load time by 30–50% on equivalent wait times per usability studies. The `QuotaBar` already uses `animate-pulse` — extend that pattern. | Medium | `StorageBrowser.vue`, `AppSidebar.vue`, new `SkeletonRow.vue` |
| Global search focus shortcut (`/` key) | Table stakes for document management tools. Paperless-ngx, Notion, GitHub, Linear all use `/` to focus search. Users who type `/` and nothing happens feel the app is unpolished. | Low | `SearchBar.vue`, `FileManagerView.vue` global `keydown` listener |
| Escape key closes modals and clears search | Universal browser convention. Escape should: close `ShareModal`, close `FolderDeleteModal`, close `DocumentPreviewModal`, clear the inline folder rename input, and clear search field. Several of these already handle `keydown.escape` in inputs but not at the modal overlay level. | Low | `ShareModal.vue`, `FolderDeleteModal.vue`, `DocumentPreviewModal.vue` |
| Sidebar collapses on mobile (hamburger toggle, overlay drawer) | On viewports below 1024px the fixed 256px sidebar eats 25–40% of the viewport. The current layout has no mobile handling — sidebar overlaps content on small screens. Minimum viable mobile: hamburger button + slide-in overlay drawer. | Medium | `AppSidebar.vue`, `App.vue`, new mobile toggle state |
| Responsive document list column hiding | `StorageBrowser` grid already uses `hidden md:block` for Size and `hidden sm:block` for Modified. This is correct — verify it works and document the pattern as intentional. Ensure the icon + name + actions columns always render. | Low | `StorageBrowser.vue` (verify existing classes) |
| Upload trigger keyboard shortcut (`U` key when no input is focused) | Secondary table stakes. Windows Explorer, Nextcloud, and Dropbox support pressing a key to trigger upload when focus is on the file list. Makes keyboard-first workflows viable. | Low | `DropZone.vue` or `FileManagerView.vue` global `keydown` listener |
| Visible drag-and-drop affordance on the drop zone at all times | `DropZone.vue` already exists and handles dragover/drop. The gap: the drop zone is a full-width dashed box inside the content area, which is correct, but there is no whole-page drag overlay when a user drags a file from the OS file manager. Users miss the target and the drop is ignored. | Medium | `FileManagerView.vue` document-level dragenter/dragleave listener |
| Contextual breadcrumb with admin section label | Admin subroutes need breadcrumbs: `Admin > Users`, `Admin > Audit Log`. Without them, admin pages feel unmoored. The user-facing `FolderBreadcrumb` is already built — admin can use the same component with static segments. | Low | `FolderBreadcrumb.vue`, `AdminLayout.vue` |

---

## Differentiators

Features that add genuine value beyond the baseline. Not expected by most users, but valued when present.

| Feature | Value Proposition | Complexity | Depends On |
|---------|-------------------|------------|------------|
| Whole-page drop zone (drag files anywhere on the screen to upload) | Removes the requirement to aim at the DropZone rectangle. Paperless-ngx supports this. Implementation: listen for `dragenter` at the document level, show a full-screen overlay with the drop target, then pipe the `drop` event into the existing upload emit chain. | Medium | `FileManagerView.vue`, new `PageDropOverlay.vue`, existing `DropZone` emit chain |
| Skeleton placeholders shaped like the real content (5-column grid rows) | The QuotaBar has a simple pulse bar. The document list skeleton should be 5-column grid rows matching the actual layout — icon placeholder, name placeholder, size placeholder, date placeholder. This makes the transition from loading to content feel instantaneous rather than a layout jump. | Medium | New `SkeletonRow.vue`, `StorageBrowser.vue` |
| Admin dashboard overview page (`/admin` root — stats at a glance) | User count, storage total across all users, recent audit log entries. Gives admins a reason to visit the admin panel rather than jumping straight to sub-sections. Self-hosted tools consistently provide this at the admin root. | Medium | New `AdminOverviewView.vue`, read-only aggregate API endpoints (may require backend work) |
| Inline toast/notification system for upload completion and errors | Currently errors appear in the UploadProgress inline list and are never dismissed. A toast system enables transient success/error messages for any action (rename, delete, share) — not just upload. Constraint: keep it simple, not a full notification center. | Medium | New `ToastManager.vue` in `App.vue`, composable `useToast()` |
| Document count badge polish on topic sidebar links | Already has `{{ topic.doc_count }}` in the sidebar. What's missing: show 0 counts as dimmed rather than hidden (users want to know the topic exists even if empty), and truncate to "9+" at double digits. | Low | `AppSidebar.vue` (minor change) |
| Upload from keyboard shortcut `U` with accessible file picker trigger | The keyboard shortcut fires `triggerInput()` on DropZone. Accessibility requirement: this is equivalent to clicking the hidden file input — screen readers announce the file dialog opening. Already partially supported by `DropZone.vue`'s `triggerInput()` — wire up the global shortcut. | Low | `DropZone.vue`, `FileManagerView.vue` |
| Tablet landscape layout: sidebar + content, no collapse needed | At 1024px (lg breakpoint), the sidebar is fully visible alongside content. This is the natural Tailwind `lg:flex` pattern. Verify it and make the breakpoint logic explicit in the layout wrapper. | Low | `App.vue` layout wrapper |

---

## Anti-Features

Features to explicitly NOT build in v0.2.

| Anti-Feature | Why Avoid | What to Do Instead |
|--------------|-----------|-------------------|
| Folder reorder by drag-and-drop (drag folder A above folder B to change sidebar order) | Requires a persistent `position` column in the DB, a backend migration, and drag state tracking across the sidebar tree. The sidebar is already collapsible. Payoff is marginal. | Keep alphabetical or creation-time ordering. Users can rename folders to impose their own order (e.g., "1. Contracts"). |
| Reorderable sidebar sections (drag "Folders" above "Cloud Storage") | Same DB persistence problem. Section order is fixed and meaningful — user folders first, then external cloud. No self-hosted document manager reorders sections. | Fixed section order in `AppSidebar.vue`. |
| Virtual scrolling for the document list | DocuVault is quota-capped at 100 MB per user. At typical document sizes (10–500 KB each), a user has at most a few hundred documents before hitting quota. Virtual scrolling adds significant complexity (dynamic row height, scroll restoration) for an edge case that the quota cap prevents. | Standard `v-for`. Add server-side pagination in a later milestone if needed. |
| Rich text document annotations or in-app editing | Already scoped out in `PROJECT.md`. Mentioned here because UX redesigns often attract "what if we added..." scope creep. The preview modal shows PDF; that is the limit. | Stay read-only. Document content is immutable after upload. |
| Dark mode toggle | Requires a systematic color token system across all components plus full testing of the inverted palette. The current light-mode palette is inconsistent — fix that first. Tailwind makes dark mode mechanically possible but the v0.2 payoff is low relative to scope. | Design tokens and CSS variables in v0.2 that would support dark mode in a future milestone. |
| Drag-and-drop to reorder documents within a folder | Requires a persistent sort order column per user per folder. Documents are naturally sorted by creation date or name — both useful orderings. Custom ordering has high complexity and low frequency of use in a document vault. | Keep the existing sort controls (created_at, name, size). Server-side sort is already implemented. |
| Full-screen modal for file upload | The DropZone is embedded in the file browser content area, which is the correct placement. A modal upload workflow adds navigation overhead with no benefit for a simple file picker. | The whole-page drag overlay (differentiator above) is the right scope. |
| Notification center / inbox for async events | Building a polling-based notification center (classification done, quota warning) is a full feature, not UX polish. Celery classification runs in the background and its result is visible when the user next opens the document. | Toast on upload completion. Badge count on the document list is sufficient for v0.2. |
| Multi-select with batch operations (select 10 files, delete all) | Requires a selection state layer across the file list, a selection-aware toolbar, and confirmed batch-delete UX. Medium-to-high complexity for a feature that is rarely needed in a personal document vault. | Address in a future milestone if user feedback identifies it as a gap. |
| Drag documents from file list to sidebar folder entries | Cross-component drag state requires either a Pinia drag store or provide/inject. The sidebar `FolderTreeItem` and `StorageBrowser` have no shared parent that can coordinate state without significant refactoring. The "Move to folder" picker button already covers this use case. | The existing folder picker button in the file row actions menu. |

---

## Admin Panel: Specific Pattern Decisions

### Route structure

```
/admin                    → AdminLayout.vue wrapper (persistent sidebar + router-view)
  /admin/                 → AdminOverviewView.vue (stats at a glance)
  /admin/users            → AdminUsersView.vue (wraps AdminUsersTab.vue)
  /admin/quotas           → AdminQuotasView.vue (wraps AdminQuotasTab.vue)
  /admin/ai               → AdminAiConfigView.vue (wraps AdminAiConfigTab.vue)
  /admin/audit            → AdminAuditView.vue (wraps AuditLogTab.vue)
```

The existing tab components (`AdminUsersTab.vue`, `AdminQuotasTab.vue`, `AdminAiConfigTab.vue`, `AuditLogTab.vue`) are promoted to route-level views. The `AdminLayout.vue` replaces `AdminView.vue`. The tab strip becomes a sidebar with router-link active styling. No logic changes inside the tab components — only the container changes. `AdminView.vue` is deleted after the migration.

Vue Router 4 nested routes pattern (HIGH confidence — verified against official docs):

```js
{
  path: '/admin',
  component: AdminLayout,   // contains <router-view />
  meta: { requiresAdmin: true },
  children: [
    { path: '', component: AdminOverviewView },
    { path: 'users', component: AdminUsersView },
    { path: 'quotas', component: AdminQuotasView },
    { path: 'ai', component: AdminAiConfigView },
    { path: 'audit', component: AdminAuditView },
  ],
}
```

The `requiresAdmin` guard moves from the child route to the parent. All children inherit it automatically.

### Admin sidebar nav items (in order)
1. Overview
2. Users
3. Quotas
4. AI Config
5. Audit Log

A "Back to app" link at the bottom of the admin sidebar returns to `/` — admin should not feel like a dead end.

### Admin sidebar does NOT share code with `AppSidebar.vue`

`AdminSidebar.vue` is a separate component. It has no folder tree, no cloud tree, no topics list, no quota bar. Those belong to the user interface. Sharing the sidebar component would create coupling between two logically separate contexts and conflict with the "thin view + smart component" architecture described in CLAUDE.md.

### Breadcrumb

Use the existing `FolderBreadcrumb.vue` or a simpler static variant: `Admin > [Section Name]`. No dynamic nesting required. The admin sidebar's active link already signals location; the breadcrumb adds skimmability for users who navigate via URL.

---

## Empty State Patterns: Specific Decisions

Each zero-content context gets a distinct implementation. Generic "No items" text is the anti-pattern.

| Context | Headline | Subtext | CTA |
|---------|----------|---------|-----|
| Root folder (no folders yet) | "Start organizing your documents" | "Create a folder to group related files together." | "New folder" button |
| Folder is empty (exists but has no contents) | "This folder is empty" | "Upload files here or move documents into this folder." | "Upload files" link (triggers DropZone click) |
| Search returns no results | "No matches for '{query}'" | "Try a different search term or clear your search." | "Clear search" button |
| Shared with me (no shares yet) | "Nothing shared with you yet" | "When someone shares a document with your handle, it appears here." | None (passive state) |
| Topics list empty | "No topics yet" | "Topics are created automatically when you upload and classify a document." | None |
| Audit log (admin, no entries yet) | "No audit events recorded" | "Events are recorded when users log in, upload documents, or make changes." | None |
| Cloud storage — no connections | "No cloud storage connected" | "Connect OneDrive, Google Drive, Nextcloud, or WebDAV in Settings." | "Open Settings" link |

Rule: every empty state that the user can resolve with an action gets exactly one CTA. Empty states that are passive (shared-with-me before anyone shares, topics before first upload, audit before any events) get explanatory subtext only — no CTA that leads nowhere useful.

Implementation: extract a shared `EmptyState.vue` presentational component that accepts `headline`, `subtext`, and an optional `action` slot. Use it in all seven contexts above instead of duplicating the layout.

---

## Loading State Patterns: Specific Decisions

| Component | Current | Target | Rationale |
|-----------|---------|--------|-----------|
| Document list (`StorageBrowser`) | `<div>Loading…</div>` text | 6–8 skeleton rows (5-col grid, `animate-pulse`) | List loads on every folder navigation. Skeletons match the grid layout exactly, preventing layout jump. |
| Sidebar folder tree | `<div>Loading…</div>` text | 3 skeleton nav items (indent + placeholder bar) | Sidebar loads once on mount. Short skeleton is sufficient. |
| Sidebar topics list | `<div>Loading…</div>` text | 4 skeleton topic pills (color dot + bar) | Same rationale as folder tree. |
| Admin user table | No loading state | Skeleton table rows (3–5 rows, matching column count) | Admin tables are data-heavy; skeleton preserves table chrome during load. |
| Audit log table | No loading state | Skeleton rows matching timestamp/user/action columns | Same. |
| Upload progress item | Spinner + progress bar (correct) | Keep as-is | Upload is a blocking action, not a content load. Spinners are correct for actions. |
| QuotaBar | `animate-pulse` bar (correct) | No change | Already implemented correctly. Document as the canonical skeleton pattern. |
| Document preview modal | None | Spinner centered in modal body | PDF proxy fetch is a single-request action. Spinner is correct. |

Rule: if the loading UI represents content the user will read (list, table, tree), use a skeleton. If the loading UI represents a system action (save, upload, submit, process), use a spinner. Never show a loading state that will be visible for less than 300ms — add a minimum display delay.

---

## Drag-and-Drop: Scope Boundaries

### Build in v0.2

1. **Whole-page drop overlay for file upload** — listen for `dragenter` at the document level, show a full-screen overlay, delegate the `drop` event to the existing DropZone emit chain. The drop must only activate when files are being dragged (check `e.dataTransfer.types.includes('Files')`).

2. **Move a document to a folder by dragging the file row onto a folder row** — already implemented in `StorageBrowser.vue`. Verify it functions correctly and polish the drop highlight: `ring-2 ring-inset ring-amber-300` on the folder row is correct. Ensure the file row shows reduced opacity during drag (`opacity-50` already applied).

### Do Not Build in v0.2

- Dragging files from the file list onto sidebar folder entries (requires cross-component drag state via Pinia or provide/inject — the "Move to folder" picker already covers this)
- Touch drag-and-drop on mobile (HTML5 drag events do not fire on iOS Safari without a polyfill library; defer to the move picker button)
- Drag folders to reorder them

### Required for every drag interaction

- Visual affordance on dragover (already done on DropZone; add to whole-page overlay)
- Non-drag alternative for every drag action: the "Move to folder" picker button covers the drag-to-move case
- Drop must be clearly confirmed: file row disappears from source location and appears in target on success

---

## Keyboard Shortcuts: Final List

| Shortcut | Action | Condition |
|----------|--------|-----------|
| `/` | Focus search bar | Not in an input, textarea, or contenteditable |
| `Escape` | Close modal or clear active inline input | Modal open, or search focused with content |
| `U` | Trigger file picker (upload) | Not in any input, on a folder/file manager view |
| `N` | Start new folder inline input | Not in any input, on a folder/file manager view |

### Do Not Build

- `J`/`K` for row-by-row navigation — requires focus management, ARIA selected state, and keyboard event bubbling control. Over-engineered for a vault where opening a document is secondary to browsing and uploading.
- `Ctrl+A` / `Cmd+A` for select all — requires multi-select, which is an anti-feature.
- Arrow key tree navigation in the sidebar — screen readers handle this natively; custom arrow-key navigation would conflict with browser defaults.
- `?` shortcut for a shortcut reference modal — too few shortcuts to warrant a help overlay.

---

## Responsive Layout: Breakpoint Decisions

| Viewport | Tailwind breakpoint | Sidebar | Document list | Admin |
|----------|---------------------|---------|--------------|-------|
| Mobile portrait | `< 640px` (below `sm`) | Hidden; hamburger button + overlay drawer | Icon + name only; Size (`hidden md:block`) and Modified (`hidden sm:block`) hidden | Same drawer |
| Mobile landscape / small tablet | `640–767px` (`sm`) | Hidden; hamburger still active | Icon + name + Modified visible; Size hidden | Same |
| Tablet portrait | `768–1023px` (`md`) | Hidden; hamburger still active | Full 5-col grid | Full columns |
| Tablet landscape / small desktop | `1024px+` (`lg`) | Always visible (256px fixed) | Full 5-col grid | Full admin sidebar + content |
| Desktop | `1280px+` (`xl`) | Always visible | Full grid + comfortable padding | Full layout |

Key decision: sidebar collapses at the `lg` breakpoint (1024px), using `hidden lg:flex` on the sidebar and `lg:hidden` on the hamburger button. At 1024px the sidebar (256px) leaves 768px for content — sufficient for the 5-column grid with room for padding.

Touch targets: all inline action icon buttons are currently `p-1.5` (approximately 30px). Increase to `p-2` on `sm:` and below using responsive padding classes. 44px is the WCAG target — not achievable for inline buttons without breaking the grid layout, but 36px is a reasonable minimum.

---

## Feature Dependencies (summary)

```
Admin route subtree
  → AdminLayout.vue (new) contains AdminSidebar.vue (new) + <router-view>
  → Child views wrap existing AdminUsersTab, AdminQuotasTab, AdminAiConfigTab, AuditLogTab
  → AdminView.vue deleted after migration

Empty states
  → New EmptyState.vue (presentational, accepts headline/subtext/action slot)
  → Used in StorageBrowser.vue, SharedView.vue, TopicsView.vue, CloudStorageView.vue, AuditLogTab.vue

Skeleton rows
  → New SkeletonRow.vue (5-col grid, animate-pulse)
  → Used in StorageBrowser.vue (document list), AppSidebar.vue (folder tree, topics list)

Keyboard shortcuts
  → Global keydown listener in FileManagerView.vue
  → SearchBar.vue exposes focus() method
  → DropZone.vue exposes triggerInput() (already exists as triggerInput())
  → StorageBrowser.vue exposes startNewFolder() (already defineExposed)

Mobile sidebar
  → App.vue holds sidebarOpen boolean state
  → AppSidebar.vue accepts isOpen prop + emits close
  → Hamburger button in App.vue header (mobile only, hidden lg:hidden)

Whole-page drop overlay
  → New PageDropOverlay.vue (full-screen overlay, accepts active prop)
  → FileManagerView.vue listens for document dragenter/dragleave
  → On drop, delegates to existing upload flow via onFilesSelected
```

---

## Sources

- Vue Router 4 nested routes: https://router.vuejs.org/guide/essentials/nested-routes
- Skeleton screens vs. spinners UX: https://www.onething.design/post/skeleton-screens-vs-loading-spinners
- Skeleton loading screen design: https://blog.logrocket.com/ux-design/skeleton-loading-screen-design/
- SaaS empty state patterns: https://pixxen.com/saas-empty-state-design/
- IBM Carbon Design System empty states: https://carbondesignsystem.com/patterns/empty-states-pattern/
- Drag-and-drop SaaS UX: https://www.eleken.co/blog-posts/drag-and-drop-ui
- Drag-and-drop NN/g guidelines: https://www.nngroup.com/articles/drag-drop/
- Responsive SaaS breakpoints: https://tryhoverify.com/blog/responsive-design-breakpoints-2024-guide/
- Keyboard shortcut design for web apps: https://sashika.medium.com/j-k-or-how-to-choose-keyboard-shortcuts-for-web-applications-a7c3b7b408ee
- Nextcloud layout patterns: https://docs.nextcloud.com/server/latest/developer_manual/design/layout.html
- Paperless-ngx frontend architecture: https://deepwiki.com/paperless-ngx/paperless-ngx/2-frontend-architecture
