Skip to content

Main Application Component

Relevant source files

The following files were used as context for generating this wiki page:

Purpose and Scope

This document covers App.vue, the root application component that serves as the orchestrator for the entire KanStack frontend. It coordinates state management across multiple composables, handles global keyboard shortcuts and menu actions, implements the undo/redo system, and manages the integration between UI components and backend operations.

For details about individual composables managed by this component, see Composables Overview. For information about child UI components, see Key Components.


Component Overview

App.vue is the single root component of the KanStack application. It does not manage application state directly—instead, it instantiates and coordinates five core composables that handle different aspects of the application:

ComposablePurposeLines
useWorkspaceWorkspace state, board/card selection, mutationssrc/App.vue:29-52
useBoardActionsBoard/card operations (create, move, delete)src/App.vue:54-59
useBoardSelectionMulti-select state and keyboard navigationsrc/App.vue:60
useActionHistoryUndo/redo stack managementsrc/App.vue:64
Local stateColumn selection, keyboard move mode, app messagessrc/App.vue:61-63

The component renders three main child components:

  • AppHeader: Navigation breadcrumb and board selection
  • BoardCanvas: The main Kanban board UI
  • CardEditorModal: Full-screen card editor overlay

Sources: src/App.vue:1-1537


Composable Orchestration Architecture

Diagram: App.vue Composable Dependencies

App.vue passes configuration functions to useBoardActions that provide read-only access to workspace state. This ensures useBoardActions remains stateless and can only read (not modify) workspace data:

typescript
const appBoardActions = useBoardActions({
    getBoardsBySlug: () => workspace.value?.boardsBySlug ?? {},
    getWorkspaceRoot: () => workspace.value?.rootPath ?? null,
    getBoardFilesBySlug: () => workspace.value?.boardFilesBySlug ?? {},
    getCardsBySlug: () => workspace.value?.cardsBySlug ?? {},
});

Sources: src/App.vue:29-60


State Management Flow

All workspace state lives in useWorkspace, but state updates flow through App.vue's coordination layer:

Diagram: State Update Flow

The applyWorkspaceMutation function is the single entry point for updating workspace state:

Sources: src/App.vue:99-132, src/App.vue:257-261


Action History and Undo/Redo System

Every mutating operation goes through the tracked action pattern, which automatically creates before/after snapshots for undo/redo:

Diagram: Tracked Action Pattern

The executeTrackedAction function encapsulates this pattern:

typescript
async function executeTrackedAction(
    label: string,
    perform: () => Promise<HistoryStateSnapshot | null>,
) {
    const before = createHistoryStateSnapshot();
    if (!before) return null;
    
    const after = await perform();
    if (!after) return null;
    
    actionHistory.push({ label, before, after });
    return after;
}

All tracked operations return a HistoryStateSnapshot containing:

FieldTypeDescription
currentBoardSlugstring | nullActive board after operation
selectedCard{slug, sourceBoardSlug} | nullSelected card after operation
selectedColumnSlugstring | nullSelected column after operation
snapshotWorkspaceSnapshotComplete workspace state

Sources: src/App.vue:99-155, src/App.vue:229-262

Undo/Redo Implementation

Undo and redo operations apply snapshots by invoking the backend command apply_workspace_snapshot, which atomically restores file system state:

typescript
async function applyHistoryStateSnapshot(state: HistoryStateSnapshot) {
    const snapshot = await invoke<WorkspaceSnapshot>(
        "apply_workspace_snapshot",
        { snapshot: state.snapshot }
    );
    
    applyWorkspaceMutation({
        snapshot,
        currentBoardSlug: state.currentBoardSlug,
        selectedCard: state.selectedCard,
    });
    selectedColumnState.value = state.selectedColumnSlug;
}

Sources: src/App.vue:118-132, src/App.vue:211-227


Global Keyboard Shortcuts

App.vue implements comprehensive keyboard shortcuts via handleGlobalKeydown and handleGlobalKeyup event listeners attached to the window:

Keyboard Shortcuts Table

ShortcutConditionActionLines
EscapeKeyboard move mode activeCancel move modesrc/App.vue:920-925
EscapeCard editor openClose editorsrc/App.vue:927-931
EscapeCards selectedClear selectionsrc/App.vue:933-937
EscapeColumn selectedClear column selectionsrc/App.vue:939-943
Cmd/Ctrl+ZNot in editable elementUndosrc/App.vue:963-967
Cmd/Ctrl+Shift+ZNot in editable elementRedosrc/App.vue:954-961
Cmd/Ctrl+YNot in editable elementRedosrc/App.vue:969-973
Space1 card selected + reorder enabledEnter card move modesrc/App.vue:977-986
SpaceColumn selectedEnter column move modesrc/App.vue:988-994
Arrow KeysCard move modeMove selected cardsrc/App.vue:1008-1022
Arrow Left/RightColumn move modeMove selected columnsrc/App.vue:997-1005
Arrow KeysColumn selectedNavigate columnssrc/App.vue:1024-1031
Arrow KeysNormal modeNavigate card selectionsrc/App.vue:1033-1047
Delete/BackspaceCards selectedArchive cardssrc/App.vue:1051-1060
Shift+DeleteCards selectedDelete cards (with confirm)src/App.vue:1055-1060
Delete/BackspaceColumn selectedDelete columnsrc/App.vue:1063-1070
Enter1 card selectedOpen card editorsrc/App.vue:1073-1080
Cmd/Ctrl+O-Open workspacesrc/App.vue:1086-1090
Cmd/Ctrl+Shift+N-Create new boardsrc/App.vue:1092-1096
Cmd/Ctrl+N-Create new cardsrc/App.vue:1098-1102
Cmd/Ctrl+Shift+A-Toggle archive columnsrc/App.vue:1104-1107

The keyboard handler checks if focus is in an editable element and skips most shortcuts if so:

typescript
function isEditableElement(target: Element | null) {
    if (!(target instanceof HTMLElement)) return false;
    
    const tagName = target.tagName.toLowerCase();
    return (
        tagName === "input" ||
        tagName === "textarea" ||
        tagName === "select" ||
        target.isContentEditable
    );
}

Sources: src/App.vue:912-1108, src/App.vue:1203-1215


Native application menus (built in src-tauri/src/main.rs) trigger actions that flow through an event-based system:

Diagram: Menu Action Flow

App.vue listens for menu-action events during mount:

typescript
async function attachMenuActionListener() {
    unlistenMenuActions = await listen<{ action: string }>(
        "menu-action",
        (event) => {
            void dispatchMenuAction(event.payload.action);
        }
    );
}

The dispatchMenuAction function maps action strings to local functions:

Sources: src/App.vue:1122-1201, src-tauri/src/main.rs:27-31, src-tauri/src/main.rs:161-180


Keyboard Move Modes

KanStack supports two keyboard-driven move modes for drag-free reordering:

  1. Card Move Mode: Press Space with one card selected to enter mode, then use arrow keys to move the card between columns and positions
  2. Column Move Mode: Press Space with a column selected to enter mode, then use left/right arrows to reorder columns

Diagram: Keyboard Move Mode State Machine

The keyboardMoveMode ref tracks which mode is active:

Sources: src/App.vue:62, src/App.vue:977-1005, src/App.vue:1110-1120


Tracked Operation Functions

App.vue implements numerous tracked operations that follow the pattern shown earlier. Here are key examples:

Board and Column Operations

FunctionLabelOperationLines
createCardFromBoard"New Card"Creates card in current boardsrc/App.vue:229-262
createColumn"New Column"Adds column to current boardsrc/App.vue:264-294
handleColumnReorder"Reorder Columns"Reorders columns via dragsrc/App.vue:296-332
toggleArchiveColumn"Show/Hide Archive Column"Toggles archive column visibilitysrc/App.vue:334-375
handleColumnRename"Rename Column"Renames column across workspacesrc/App.vue:437-475
handleBoardRename"Rename Board"Renames current boardsrc/App.vue:477-509

Card Operations

FunctionLabelOperationLines
moveCardTracked"Move Card"Moves card to different column/sectionsrc/App.vue:554-610
archiveSelectedCards"Archive Cards"Archives multiple selected cardssrc/App.vue:612-669
deleteSelectedCards"Delete Cards"Permanently deletes selected cardssrc/App.vue:676-751

Destructive Operations

Some operations perform confirmation prompts before executing:

typescript
async function deleteCurrentBoard() {
    if (!currentBoard.value || !workspace.value?.rootPath) {
        return;
    }
    
    const descendantCount = countDescendantBoards(currentBoard.value.slug);
    const confirmationMessage = descendantCount > 0
        ? `Delete board "${currentBoard.value.title}" and its ${descendantCount} sub board${descendantCount === 1 ? "" : "s"}? ...`
        : `Delete board "${currentBoard.value.title}"? ...`;
        
    const confirmed = window.confirm(confirmationMessage);
    if (!confirmed) return;
    
    // ... proceed with deletion
}

Sources: src/App.vue:758-810, src/App.vue:676-751


Integration with Child Components

App.vue renders three main child components and handles their events:

BoardCanvas Integration

Props Passed:

typescript
<BoardCanvas
    :board="currentBoard"
    :boards-by-slug="workspace?.boardsBySlug ?? {}"
    :board-files-by-slug="workspace?.boardFilesBySlug ?? {}"
    :cards-by-slug="workspace?.cardsBySlug ?? {}"
    :selected-column-slug="selectedColumnSlug"
    :selected-card-keys="boardSelection.selectedKeys.value"
    :view-preferences="viewPreferences"
    :workspace-root="workspace?.rootPath ?? null"
/>

Events Handled:

EventHandlerPurpose
@activate-cardhandleCardActivateMulti-select card with modifiers
@add-columncreateColumnCreate new column
@clear-selectionsclearSelectionsClear all selections
@create-cardcreateCardFromBoardCreate new card
@move-cardhandleCardMoveDrag-drop card move
@open-cardopenCardOpen card editor
@reorder-columnshandleColumnReorderDrag-drop column reorder
@rename-boardhandleBoardRenameRename current board
@rename-columnhandleColumnRenameRename column
@select-columnhandleColumnSelectSelect column
@toggle-archive-columntoggleArchiveColumnToggle archive visibility
@update-view-preferencesupdateViewPreferencesUpdate view settings
@update-visible-cardshandleVisibleCardsUpdate selection registry

Sources: src/App.vue:1555-1580

CardEditorModal Integration

Props Passed:

typescript
<CardEditorModal
    :card="selectedCard"
    :board-files-by-slug="workspace?.boardFilesBySlug ?? {}"
    :boards-by-slug="workspace?.boardsBySlug ?? {}"
    :cards-by-slug="workspace?.cardsBySlug ?? {}"
    :open="Boolean(selectedCardSlug)"
    :source-board="selectedCardSourceBoard"
    :workspace-root="workspace?.rootPath ?? null"
/>

Events Handled:

EventHandlerPurpose
@archive-cardarchiveSingleCardArchive single card
@apply-workspace-mutationapplyWorkspaceMutationApply editor changes
@closecloseCardClose editor
@delete-carddeleteSingleCardDelete single card

The modal directly calls applyWorkspaceMutation when the user saves changes, bypassing the tracked action system since the card editor has its own undo/redo mechanism.

Sources: src/App.vue:1612-1625


Lifecycle and Event Management

App.vue attaches global event listeners during mount and cleans them up during unmount:

typescript
onMounted(() => {
    void restoreWorkspace();
    void attachMenuActionListener();
    window.addEventListener("keydown", handleGlobalKeydown);
    window.addEventListener("keyup", handleGlobalKeyup);
});

onUnmounted(() => {
    clearAppMessageTimer();
    if (unlistenMenuActions) {
        unlistenMenuActions();
        unlistenMenuActions = null;
    }
    window.removeEventListener("keydown", handleGlobalKeydown);
    window.removeEventListener("keyup", handleGlobalKeyup);
});

On mount, it also:

  • Restores the previously opened workspace from persisted app config
  • Attaches a listener for menu action events from the Rust backend

Sources: src/App.vue:193-209


App Message System

App.vue manages transient notification messages displayed to users via AppMessageBanner:

typescript
function showAppMessage(text: string) {
    clearAppMessageTimer();
    appMessage.value = { kind: "error", text };
    appMessageTimer = window.setTimeout(() => {
        appMessage.value = null;
        appMessageTimer = null;
    }, 5000);
}

Messages auto-dismiss after 5 seconds or can be manually dismissed. Common use cases:

  • Board attachment confirmations
  • Missing known board notifications
  • Operation validation errors (e.g., cannot rename Archive column)

Sources: src/App.vue:873-910


Summary

App.vue serves as the coordination layer between composables, UI components, and the backend. Its responsibilities include:

  1. Composable orchestration: Instantiating and configuring all core composables
  2. Action tracking: Wrapping operations in the executeTrackedAction pattern for undo/redo
  3. Event handling: Processing keyboard shortcuts and menu actions
  4. State synchronization: Coordinating state changes across composables and local state
  5. Component integration: Managing props and events for child components
  6. Lifecycle management: Attaching/detaching global event listeners

This architecture keeps the component focused on coordination rather than business logic, with actual operations delegated to specialized composables.

Sources: src/App.vue:1-1537