skills/cin12211/orca-q/primary-sidebar

primary-sidebar

Installation
SKILL.md

Primary Sidebar Flow — OrcaQ

Architecture Overview

The Primary Sidebar is driven by a single active tab value in a Pinia store. The flow is:

ActivityBarItemType (enum)           ← Tab identity
useActivityBarStore.activityActive   ← Which tab is currently active (persisted)
PrimarySideBar.vue                   ← Watches activityActive, renders the matching component
Management***.vue                    ← The actual panel content (KeepAlive'd)

The Activity Bar (the narrow icon strip on the far left) calls setActivityActive(type). The Primary Sidebar reacts to the change and swaps the rendered panel — all panels are wrapped in <KeepAlive> so their state is preserved when the user switches tabs.


File Locations

Purpose File
Tab type enum + store core/stores/useActivityBarStore.ts
Sidebar shell (switcher) components/modules/app-shell/primary-side-bar/components/PrimarySideBar.vue
Sidebar public API components/modules/app-shell/primary-side-bar/index.ts
All management panels components/modules/management/
Management public API components/modules/management/index.ts
Shared header component components/modules/management/shared/components/ManagementSidebarHeader.vue

Management panel locations

Tab Panel component file
Explorer components/modules/management/explorer/ManagementExplorer.vue
Schemas components/modules/management/schemas/ManagementSchemas.vue
ERD Diagram components/modules/management/erd-diagram/ManagementErdDiagram.vue
Users & Roles components/modules/management/role-permission/ManagementUsersAndPermission.vue
Export components/modules/management/export/ManagementExport.vue
Agent components/modules/management/agent/ManagementAgent.vue

How the Switcher Works (PrimarySideBar.vue)

<script setup lang="ts">
const activityStore = useActivityBarStore();
const current = shallowRef();

watch(
  () => activityStore.activityActive,
  () => {
    if (activityStore.activityActive === ActivityBarItemType.Explorer)
      current.value = ManagementExplorer;
    if (activityStore.activityActive === ActivityBarItemType.Schemas)
      current.value = ManagementSchemas;
    // ... one branch per tab
  },
  { immediate: true }
);
</script>

<template>
  <div class="w-full h-full flex flex-col" v-if="appConfigStore.layoutSize[0]">
    <KeepAlive>
      <component :is="current" />
    </KeepAlive>
  </div>
</template>

Key points:

  • Uses shallowRef (not ref) for the component — avoids deep reactivity on component objects.
  • immediate: true so the correct panel is rendered on first mount.
  • <KeepAlive> preserves scroll position and internal state when switching tabs.
  • The panel is only mounted when the sidebar is open (layoutSize[0] > 0).

How to Add a New Sidebar Tab

Step 1 — Add enum value

In core/stores/useActivityBarStore.ts:

export enum ActivityBarItemType {
  Explorer = 'Explorer',
  Schemas = 'Schemas',
  ErdDiagram = 'ERDiagram',
  UsersRoles = 'UsersRoles',
  DatabaseExport = 'DatabaseExport',
  Agent = 'Agent',
  MyNewTab = 'MyNewTab', // ← new
}

Step 2 — Create the management panel module

Create the folder components/modules/management/my-new-tab/ with this structure:

my-new-tab/
├── index.ts                    ← exports ManagementMyNewTab
├── ManagementMyNewTab.vue      ← entry component
├── components/                 ← sub-components (optional)
├── hooks/                      ← business logic composables (optional)
└── services/                   ← API calls (optional)

ManagementMyNewTab.vue minimum template:

<script setup lang="ts">
import { ManagementSidebarHeader } from '../shared';
</script>

<template>
  <div class="flex flex-col h-full w-full overflow-y-auto">
    <ManagementSidebarHeader title="My New Tab" />
    <!-- panel content here -->
  </div>
</template>

index.ts:

export { default as ManagementMyNewTab } from './ManagementMyNewTab.vue';

Step 3 — Export from the management module

In components/modules/management/index.ts:

export * from './my-new-tab'; // ← add this line

Step 4 — Register in PrimarySideBar

In components/modules/app-shell/primary-side-bar/components/PrimarySideBar.vue:

// 1. Import the component
import { ManagementMyNewTab } from '#components';

// 2. Add a branch in the watch
watch(
  () => activityStore.activityActive,
  () => {
    // ... existing branches ...
    if (activityStore.activityActive === ActivityBarItemType.MyNewTab)
      current.value = ManagementMyNewTab;
  },
  { immediate: true }
);

Step 5 — Add Activity Bar button

The Activity Bar icon strip that calls setActivityActive is separate from the management module. Find the component that renders the icon list and add a button:

activityStore.setActivityActive(ActivityBarItemType.MyNewTab);

How to Update an Existing Panel

  1. Find the panel in components/modules/management/<tab-name>/Management<Tab>.vue.
  2. Business logic (API calls, state) belongs in a hook under <tab-name>/hooks/.
  3. Static structure (sub-components) belongs in <tab-name>/components/.
  4. Persisted UI state (expanded nodes, scroll position) goes into useActivityBarStore — see schemasExpandedState, schemaCurrentScrollTop as examples.

ManagementSidebarHeader — Shared Header Component

All panels use the shared header. Props:

Prop Type Default Purpose
title string required Panel title text
showConnection boolean false Show ConnectionSelector dropdown
showSchema boolean false Show SchemaSelector dropdown
workspaceId string Required when showConnection is true
showSearch boolean false Show search input
searchPlaceholder string 'Search...' Input placeholder

It also accepts a v-model:search for two-way search binding and an #actions slot for icon buttons in the title bar.

Standard usage:

<ManagementSidebarHeader
  title="My Panel"
  :show-connection="true"
  :workspaceId="workspaceId"
  :show-search="true"
  v-model:search="searchInput"
>
  <template #actions>
    <Button size="iconSm" variant="ghost" @click="onRefresh">
      <Icon name="hugeicons:refresh" class="size-4!" />
    </Button>
  </template>
</ManagementSidebarHeader>

Controlling the Sidebar Programmatically

import {
  useActivityBarStore,
  ActivityBarItemType,
} from '~/core/stores/useActivityBarStore';

const activityStore = useActivityBarStore();

// Switch to a tab
activityStore.setActivityActive(ActivityBarItemType.Schemas);

// Read current active tab
const isSchemas = activityStore.activityActive === ActivityBarItemType.Schemas;

The sidebar visibility is controlled by appConfigStore.layoutSize[0]. Use onToggleActivityBarPanel() from useAppConfigStore to open/close it — do NOT manipulate layoutSize directly.


Hook Pattern for Panel Logic

Every non-trivial panel delegates logic to a hook. The hook receives UI callbacks via parameter (avoid importing component refs directly):

// hooks/useMyPanelTree.ts
export function useMyPanelTree(callbacks: {
  focusNode: (id: string) => void;
  collapseAll: () => void;
  expandAll: () => void;
  isExpandedAll: ComputedRef<boolean>;
}) {
  // state, API calls, event handlers
  return {
    treeData,
    searchInput,
    onClickNode,
    // ...
  };
}

Usage in the container:

<script setup lang="ts">
const treePanelRef = useTemplateRef<TreeInstance | null>('treePanelRef');

const { treeData, searchInput, onClickNode } = useMyPanelTree({
  focusNode: id => treePanelRef.value?.focusItem(id),
  collapseAll: () => treePanelRef.value?.collapseAll(),
  expandAll: () => treePanelRef.value?.expandAll(),
  isExpandedAll: computed(() => treePanelRef.value?.isExpandedAll ?? false),
});
</script>

Key Constraints

  • Never add business logic directly in PrimarySideBar.vue — it is a pure switcher. All logic lives in the management panel or its hooks.
  • Always use shallowRef for the current componentref() causes deep reactivity overhead on component objects.
  • Always wrap the <component :is="current"> in <KeepAlive> — this preserves scroll position and tree expand state when switching tabs.
  • Persisted tree state (expanded keys, scroll positions) goes into useActivityBarStore, not into local component state.
  • Imports in PrimarySideBar.vue use #components (Nuxt auto-import barrel) — not direct relative paths.
  • Each management sub-module must have its own index.ts exporting its root component, and be re-exported from components/modules/management/index.ts.
Weekly Installs
6
Repository
cin12211/orca-q
GitHub Stars
181
First Seen
Mar 28, 2026