Files
kite/.planning/research/FEATURES.md
T
curo1305andClaude Sonnet 4.6 f7758776ec docs: define milestone v0.2 requirements (36 reqs, 6 categories)
UI overhaul, codebase quality, admin panel rearchitecture, UX
improvements, responsive layout, and performance/stack updates.
Research: STACK.md, FEATURES.md, ARCHITECTURE.md, PITFALLS.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 09:22:40 +02:00

23 KiB
Raw Blame History

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 3050% 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 2540% 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 (10500 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):

{
  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 68 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 (35 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 640767px (sm) Hidden; hamburger still active Icon + name + Modified visible; Size hidden Same
Tablet portrait 7681023px (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