179 lines
8.1 KiB
Markdown
179 lines
8.1 KiB
Markdown
# Frontend Architecture — Verstak GUI
|
|
|
|
## Tech Stack
|
|
|
|
- **Framework**: Svelte 4 (runes-free, compiler-only)
|
|
- **Build tool**: Vite 5
|
|
- **GUI shell**: Wails v2 (Go backend + WebKit frontend)
|
|
- **Language**: Plain JavaScript (no TypeScript in Svelte files — `lang="ts"` is NOT used)
|
|
|
|
## Source Structure
|
|
|
|
```
|
|
frontend/src/
|
|
main.js # Entry: mounts App.svelte, global error handlers
|
|
App.svelte # Root component: sidebar, header, modals, tab router
|
|
FileTreeRow.svelte # Sidebar tree node row
|
|
TreeNode.svelte # Recursive tree node
|
|
lib/
|
|
AppHeader.svelte # Top bar with title, search, actions
|
|
BrowserEvents.svelte # Browser extension events panel
|
|
CalendarPluginPage.svelte # Plugin page host
|
|
ConfirmModal.svelte # Reusable confirm dialog
|
|
FileBreadcrumbs.svelte # File tab breadcrumb navigation
|
|
FileIcon.svelte # Icon resolver for file types
|
|
FilePreviewModal.svelte # File content preview modal
|
|
FirstRun.svelte # First-run wizard
|
|
GlobalSearch.svelte # Global search bar + results
|
|
SettingsWindow.svelte # Settings modal container
|
|
Settings*.svelte # Settings sub-sections (General, Sync, etc.)
|
|
SyncStatus.svelte # Sync status badge
|
|
TemplateIcon.svelte # Template type icon
|
|
TodayScreen.svelte # Today dashboard
|
|
VaultRecovery.svelte # Vault recovery wizard
|
|
actionIcons.js # SVG icon strings
|
|
fileUtils.js # File type helpers (canPreview, isMarkdown, etc.)
|
|
i18n/ # Internationalization (en, ru)
|
|
markdown/ # Markdown rendering + internal links
|
|
util/ # Keyboard layout helper
|
|
components/
|
|
notes/
|
|
MarkdownEditor.svelte # Markdown textarea with toolbar
|
|
MarkdownPreview.svelte # Rendered markdown preview
|
|
NoteEditorPanel.svelte # Editor + preview layout, public API: insertText()
|
|
InternalLinkPicker.svelte # Object picker for verstak:// links
|
|
ObjectPickerModal.svelte # Legacy object picker modal
|
|
```
|
|
|
|
## Role of App.svelte
|
|
|
|
`App.svelte` is the **root component**. It owns:
|
|
|
|
1. **Global UI state**: sidebar (system views, workspace tree), active tab, selected node/section
|
|
2. **Lifecycle**: startup checks (GetStartupStatus, VerstakVersion, ListWorkspaceTree), event listeners
|
|
3. **Top-level modals**: confirm, rename, import, worklog, inbox assign, link edit, settings, file preview
|
|
4. **Cross-cutting concerns**: navigation history (goBack, rememberNavigation), keyboard shortcuts, drag-and-drop orchestration, capture/inbox flow
|
|
|
|
App.svelte is **NOT** responsible for:
|
|
- Individual tab content (Overview, Notes, Files) — these are inlined but should be extracted
|
|
- Note editor internals — `NoteEditorPanel.svelte` + `MarkdownEditor.svelte` handle this
|
|
- Settings sections — each has its own `Settings*.svelte`
|
|
|
|
## State Ownership Rules
|
|
|
|
### App.svelte MUST own:
|
|
- `selectedSection`, `selectedNode`, `activeTab`
|
|
- `systemViews`, `workspaceTree`, `enabledTemplates`
|
|
- `noteEditor` (the note being edited), `noteViewMode`
|
|
- `showFirstRun`, `showRecovery`, `showSettings`, `loading`
|
|
- `startupStatus`, `startupChecked`
|
|
- Navigation state: `navHistory`, `restoringHistory`
|
|
- Trash browser state: `trashInfo`, `trashCount`, `trashSelectedIds`, `trashFolderId`, `trashFolderStack`
|
|
- Journal state: `journalRows`, `journalSummary`, filters
|
|
- Worklog modal state: `showWorklogModal`, `wlModal*`
|
|
- Inbox: `inboxNodes`, `localInboxNodes`, capture state
|
|
- Links: `links`
|
|
- Sync: `syncStatus`
|
|
- `error` (global error banner)
|
|
|
|
### App.svelte must NOT directly own (after extraction):
|
|
- Files tab internal state → `FilesTab.svelte`
|
|
- Notes tab list UI state → `NotesTab.svelte`
|
|
- Overview tab content → `OverviewTab.svelte`
|
|
|
|
## Component Communication
|
|
|
|
### Props (parent → child)
|
|
Data flows down via Svelte `export let prop`
|
|
|
|
### Events (child → parent)
|
|
Children dispatch events via `createEventDispatcher()`
|
|
- Suffix `:` in event names means handler in template: `on:openNote={handler}`, NOT call on mount
|
|
- All events should use `e.detail` to pass data
|
|
|
|
### Public API (bind:this)
|
|
Parent gets imperative handle via `bind:this={ref}` and calls:
|
|
- `ref.publicMethod(args)` — guard with optional chaining: `ref?.publicMethod?.(args)`
|
|
- Methods are exposed via `export function` in child component
|
|
|
|
## Services (Wails API)
|
|
|
|
All backend calls go through the `wailsCall()` helper in App.svelte:
|
|
|
|
```js
|
|
function wailsCall(method, ...args) {
|
|
// Returns Promise, rejects with 'Wails not connected: <method>' if backend unavailable
|
|
}
|
|
```
|
|
|
|
Key backend methods:
|
|
- **Startup**: `GetStartupStatus`, `VerstakVersion`, `ListSystemViewsWithPlugins`, `ListWorkspaceTree`, `ListEnabledTemplates`
|
|
- **Nodes**: `GetNodeDetail`, `ListItems`, `ListWorkspaceChildren`, `CreateNodeFromTemplate`, `DeleteNode`, `MoveNode`, `RenameNode`
|
|
- **Notes**: `ListNotes`, `CreateNote`, `ReadNote`, `SaveNote`, `DeleteNote`, `RenameNote`
|
|
- **Files**: `ListFiles`, `DeleteFileOrFolder`, `CreateEmptyFile`, `DuplicateNode`, `OpenFile`, `OpenFolder`, `GetFileBase64`, `ReadFileText`, `PreviewImport`, `AddPathCopy`, `AddPathLink`
|
|
- **Worklog**: `ListWorklog`, `CreateWorklogFull`, `UpdateWorklogEntry`, `DeleteWorklogEntry`, `AcceptSuggestionFull/With`, `GetSuggestions`
|
|
- **Inbox**: `ListInboxNodes`, `Capture*WithContext`, `DeleteInboxNode`, `ResolveInboxNode`
|
|
- **Sync**: `SyncStatus`, `SyncNow`, `TrashCount`, `ListTrash`
|
|
- **Logging**: `FrontendLog(level, message, stack)`, `LogStartupStep(step, success, detail)`
|
|
|
|
## Logging & Diagnostics
|
|
|
|
### Frontend errors
|
|
- Global `window.addEventListener('error', ...)` catches uncaught exceptions
|
|
- Global `window.addEventListener('unhandledrejection', ...)` catches broken promises
|
|
- Both log to `console.error` and forward to `FrontendLog()` backend binding
|
|
- Fatal mount error: renders error message inline in `#app` div (blank window becomes diagnostic)
|
|
|
|
### Backend logs
|
|
- Application log file: `~/.local/state/verstak/logs/verstak.log` (Linux)
|
|
- Fallback: `~/.config/verstak/logs/verstak.log`
|
|
- Format: `[timestamp] [level] message`
|
|
- Go `log.Printf()` output goes to stderr (visible in dev console)
|
|
|
|
### Vault debug log
|
|
- `<vault>/.verstak/debug.log` — written by `WriteDebugLog` binding (existing feature)
|
|
|
|
## Refactor Rules
|
|
|
|
### After each extraction step:
|
|
1. `grep` App.svelte for state that moved to child — must be zero hits or documented exceptions
|
|
2. `npm run build` — must pass
|
|
3. `go test ./...` — must pass
|
|
4. Manual GUI check:
|
|
- Window is not blank/white
|
|
- Sidebar shows (system views + workspace tree)
|
|
- No infinite "Загрузка..." on welcome screen
|
|
- Clicking system sections works
|
|
- Opening a case shows tabs
|
|
|
|
### Forbidden patterns:
|
|
- Child component state referenced directly in App.svelte after extraction
|
|
- `lang="ts"` in Svelte files (svelte-preprocess not installed)
|
|
- `[]string` passed directly to Wails bindings (must JSON-serialize)
|
|
- Modal without `role="dialog" aria-modal="true"` and Escape handler
|
|
- Buttons without `type="button"` in forms
|
|
- `resize: none` on textareas that should be resizable (use `resize: vertical`)
|
|
|
|
## Troubleshooting
|
|
|
|
### Blank window, no errors in terminal
|
|
1. Check browser DevTools console (F12 on Windows/Linux, Cmd+Alt+I on macOS)
|
|
2. In Wails dev mode: right-click → Inspect → Console
|
|
3. Look for `[Frontend Error]` messages
|
|
4. Check application log: `~/.local/state/verstak/logs/verstak.log`
|
|
|
|
### Stuck on "Загрузка..."
|
|
- `GetStartupStatus` likely failed or returned unexpected status
|
|
- Check network calls in DevTools Network tab
|
|
- Check backend log for startup errors
|
|
|
|
### Wails method not found
|
|
- Ensure method is registered in `bindings*.go` files
|
|
- Ensure method is exported (capitalized name)
|
|
- Ensure `wailsCall()` helper is used (not direct window access)
|
|
|
|
### Child component state moved but App still references old variable
|
|
- This causes a silent `undefined` reference or runtime crash
|
|
- Run `grep -n 'oldStateName' App.svelte` after each extraction
|
|
- Use `ref?.method?.()` for all public API calls on child components
|