Examples

Plugin Stack

Active item tracking and operation history monitoring.

A comprehensive example demonstrating the use of multiple plugins with modern UI components and improved UX:

  • Active item tracking with activeItemPlugin
  • Operation history logging with historyPlugin
  • Real-time history monitoring
  • Plugin type extensions
  • Reactive state synchronization
  • Modern UI: Button, Badge, UIcon
3 items

Items (3)

  • First ItemID: item-1
  • Second ItemID: item-2
  • Third ItemID: item-3

Active Item

No item selected

Operation History (0 / 30)

Tracks all check-ins, check-outs, and updates. Most recent operations appear first.

Project Structure

plugin-stack/
├── index.ts
├── PluginStack.vue
├── PluginStackListItem.vue
├── PluginStackActiveItemPanel.vue
└── PluginStackHistoryPanel.vue

Shared Types (index.ts)

import type { InjectionKey, Ref } from 'vue';
import type { CheckInItem, DeskCore } from '#vue-airport/composables/useCheckIn';

export interface PluginItemData {
  id: string;
  name: string;
  description: string;
}

export interface DeskWithPlugins {
  activeId?: Ref<string | number | null>;
  maxHistory?: Ref<number>;
  getActive?: () => CheckInItem<PluginItemData> | null;
  getHistory?: () => Array<{ action: string; id: string | number; timestamp: number }>;
  setActive?: (id: string | number | null) => void;
}

export interface PluginItemContext {
  pluginItems: Ref<PluginItemData[]>;
  maxHistory: Ref<number>;
}

export const PLUGIN_DESK_KEY: InjectionKey<DeskCore<PluginItemData> & PluginItemContext> =
  Symbol('pluginDesk');

export { default as PluginStack } from './PluginStack.vue';
export { default as PluginStackListItem } from './PluginStackListItem.vue';
export { default as PluginStackActiveItemPanel } from './PluginStackActiveItemPanel.vue';
export { default as PluginStackHistoryPanel } from './PluginStackHistoryPanel.vue';

Parent Component (PluginStack.vue)

<script setup lang="ts">
import { ref, nextTick, onMounted } from 'vue';
import { useCheckIn } from 'vue-airport';
import { createActiveItemPlugin, createHistoryPlugin } from '@vue-airport/plugins-base';
import {
  PluginStackListItem,
  PluginStackActiveItemPanel,
  PluginStackHistoryPanel,
  type DeskWithPlugins,
  type PluginItemContext,
  type PluginItemData,
  PLUGIN_DESK_KEY,
} from '.';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';

const itemsData = ref<Array<PluginItemData>>([
  { id: 'item-1', name: 'First Item', description: 'This is the first item in the list' },
  { id: 'item-2', name: 'Second Item', description: 'This is the second item' },
  { id: 'item-3', name: 'Third Item', description: 'This is the third item' },
]);

const maxHistory = ref(30);

const activeItemPlugin = createActiveItemPlugin<PluginItemData>();
const historyPlugin = createHistoryPlugin<PluginItemData>({ maxHistory: maxHistory.value });

const { createDesk } = useCheckIn<PluginItemData, PluginItemContext>();
const { desk } = createDesk(PLUGIN_DESK_KEY, {
  devTools: true,
  debug: false,
  plugins: [activeItemPlugin, historyPlugin],
  context: { pluginItems: itemsData, maxHistory },
  onCheckOut(id) {
    const index = itemsData.value.findIndex((item) => item.id === id);
    if (index !== -1) {
      if (deskWithPlugins.activeId?.value === id) {
        deskWithPlugins.setActive?.(null);
      }
      itemsData.value.splice(index, 1);
    }
    if (itemsData.value.length > 0) {
      const newIndex = Math.min(index, itemsData.value.length - 1);
      const id = itemsData.value[newIndex]?.id;
      if (id) {
        deskWithPlugins.setActive?.(id);
        const el = document.querySelector(`[data-slot="plugin-list-item-${id}"]`);
        if (el) (el as HTMLElement).scrollIntoView({ behavior: 'smooth', block: 'center' });
      }
    }
  },
});
const deskWithPlugins = desk as DeskWithPlugins;

const addItem = () => {
  const id = `item-${Date.now()}`;
  itemsData.value.push({
    id,
    name: `Item ${itemsData.value.length + 1}`,
    description: `Description of item ${itemsData.value.length + 1}`,
  });
  nextTick(() => {
    deskWithPlugins.setActive?.(id);
    const el = document.querySelector(`[data-slot="plugin-list-item-${id}"]`);
    if (el) (el as HTMLElement).scrollIntoView({ behavior: 'smooth', block: 'center' });
  });
};

onMounted(() => {
  const firstItem = itemsData.value[0];
  if (firstItem) deskWithPlugins.setActive?.(firstItem.id);
});
</script>

<template>
  <div>
    <div class="flex gap-3 mb-6 flex-wrap justify-between items-center">
      <Button @click="addItem">
        <UIcon name="i-heroicons-plus" class="mr-2 w-4 h-4" />
        Add Item
      </Button>
      <Badge variant="outline" class="border-primary bg-primary/20 text-primary px-3 py-1">
        {{ itemsData.length }} items
      </Badge>
    </div>
    <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
      <div class="p-4 bg-card border border-muted rounded-md">
        <h3 class="m-0 mb-4 text-base font-semibold">Items ({{ itemsData.length }})</h3>
        <ul class="list-none p-0 m-0 flex flex-col gap-2 max-h-[400px] overflow-y-auto">
          <PluginStackListItem v-for="item in itemsData" :id="item.id" :key="item.id" />
        </ul>
      </div>
      <PluginStackActiveItemPanel />
      <PluginStackHistoryPanel />
    </div>
  </div>
</template>

Child Components

PluginStackListItem.vue

<script setup lang="ts">
import { useCheckIn } from 'vue-airport';
import {
  type DeskWithPlugins,
  type PluginItemContext,
  type PluginItemData,
  PLUGIN_DESK_KEY,
} from '.';
import { Button } from '@/components/ui/button';

const props = defineProps<{ id: string }>();

const { checkIn } = useCheckIn<PluginItemData, PluginItemContext>();
const { desk } = checkIn(PLUGIN_DESK_KEY, {
  id: props.id,
  autoCheckIn: true,
  watchData: true,
  data: (desk) => {
    const item = desk.pluginItems?.value.find((i) => i.id === props.id);
    return {
      id: props.id,
      name: item?.name || '',
      description: item?.description || '',
      isActive: (desk as DeskWithPlugins).activeId?.value === props.id,
    };
  },
});
const deskWithPlugins = desk as typeof desk & DeskWithPlugins;
const data = computed(() => deskWithPlugins?.pluginItems.value.find((item) => item.id === props.id));
const isActive = computed(() => deskWithPlugins?.activeId?.value === props.id);
const setActive = () => deskWithPlugins.setActive?.(props.id);
const remove = () => deskWithPlugins.checkOut?.(props.id);
</script>

<template>
  <li
    :data-slot="`plugin-list-item-${props.id}`"
    class="flex items-center justify-between p-3 border border-muted rounded-md cursor-pointer transition-all duration-200 hover:bg-accent dark:hover:bg-accent-dark"
    :class="{ 'bg-primary-50 dark:bg-primary-900/20 border-primary-500': isActive }"
    @click="setActive"
  >
    <div class="flex flex-col gap-1">
      <strong>{{ data?.name }}</strong>
      <span class="text-xs text-gray-600 dark:text-gray-400">ID: {{ data?.id }}</span>
    </div>
    <Button
      size="icon"
      aria-label="Remove item"
      class="bg-transparent hover:bg-transparent border-0 text-destructive/80 hover:text-destructive"
      @click.stop="remove"
    >
      <UIcon name="i-heroicons-trash" class="w-4 h-4" />
    </Button>
  </li>
</template>

PluginStackActiveItemPanel.vue

<script setup lang="ts">
import { computed } from 'vue';
import { useCheckIn } from 'vue-airport';
import {
  PLUGIN_DESK_KEY,
  type DeskWithPlugins,
  type PluginItemContext,
  type PluginItemData,
} from '.';

const { checkIn } = useCheckIn<PluginItemData, PluginItemContext>();
const { desk } = checkIn(PLUGIN_DESK_KEY);
const deskWithPlugins = desk as DeskWithPlugins;
const activeId = computed(() => deskWithPlugins.activeId?.value);
const activeItem = computed(() => deskWithPlugins.getActive?.());
</script>

<template>
  <div class="p-4 bg-card border border-muted rounded-md">
    <h3 class="m-0 mb-4 text-base font-semibold">Active Item</h3>
    <div v-if="activeItem" class="space-y-2">
      <p class="my-2"><strong>ID:</strong> {{ activeId }}</p>
      <p class="my-2"><strong>Name:</strong> {{ activeItem.data.name }}</p>
      <p class="my-2"><strong>Description:</strong> {{ activeItem.data.description }}</p>
      <p class="my-2 text-sm text-muted">
        <strong>Timestamp:</strong>
        {{ new Date(activeItem.timestamp!).toLocaleString() }}
      </p>
    </div>
    <div v-else class="py-8 text-center text-muted">No item selected</div>
  </div>
</template>

PluginStackHistoryPanel.vue

<script setup lang="ts">
import { computed } from 'vue';
import { useCheckIn } from 'vue-airport';
import {
  PLUGIN_DESK_KEY,
  type DeskWithPlugins,
  type PluginItemContext,
  type PluginItemData,
} from '.';

const { checkIn } = useCheckIn<PluginItemData, PluginItemContext>();
const { desk } = checkIn(PLUGIN_DESK_KEY);
const deskWithPlugins = desk as DeskWithPlugins;
const history = computed(() => deskWithPlugins?.getHistory?.() || []);
const maxHistory = computed(() => deskWithPlugins?.maxHistory?.value || 20);

const formatAction = (action: string) => {
  const actionMap: Record<string, string> = {
    'check-in': 'Registered',
    'check-out': 'Unregistered',
    update: 'Updated',
  };
  return actionMap[action] || action;
};
</script>

<template>
  <div class="md:col-span-2 p-4 bg-card border border-muted rounded-md">
    <h3 class="m-0 mb-4 text-base font-semibold">
      Operation History ({{ history.length }} / {{ maxHistory }})
    </h3>
    <p class="mt-0 mb-4 text-sm text-muted">
      Tracks all check-ins, check-outs, and updates. Most recent operations appear first.
    </p>
    <ul class="list-none p-0 m-0 flex flex-col gap-2 max-h-[200px] overflow-y-auto">
      <li
        v-for="(entry, index) in history.slice().reverse()"
        :key="index"
        class="flex items-center gap-3 p-2 border-b border-border rounded text-sm"
      >
        <UIcon
          :name="
            entry.action === 'check-in'
              ? 'i-heroicons-plus-circle'
              : entry.action === 'check-out'
                ? 'i-heroicons-minus-circle'
                : 'i-heroicons-arrow-path'
          "
          :class="
            entry.action === 'check-in'
              ? 'text-green-500'
              : entry.action === 'check-out'
                ? 'text-red-500'
                : 'text-blue-500'
          "
        />
        <span class="font-medium min-w-[100px]">{{ formatAction(entry.action) }}</span>
        <span class="flex-1 font-mono">{{ entry.id }}</span>
        <span class="text-gray-600 dark:text-gray-400 text-xs">
          {{ new Date(entry.timestamp).toLocaleTimeString() }}
        </span>
      </li>
    </ul>
  </div>
</template>

Key Concepts

Multiple Plugins

VueAirport allows combining multiple plugins to extend desk functionality:

const activeItemPlugin = createActiveItemPlugin<PluginItemData>();
const historyPlugin = createHistoryPlugin<PluginItemData>({ maxHistory: 20 });

createDesk(PLUGIN_DESK_KEY, {
  plugins: [activeItemPlugin, historyPlugin],
});

Active Item Plugin

Tracks the currently selected item:

  • setActive(id): Set the active item
  • getActive(): Get the active item
  • activeId: Reactive reference to the active ID

History Plugin

Maintains a log of all desk operations:

  • getHistory(): Get the complete operation history
  • getLastHistory(count): Get the last N operations
  • getHistoryByAction(action): Filter history by action type
  • clearHistory(): Clear all history entries
  • history: Reactive reference to the history array

Type Extensions

Use TypeScript type extensions to access plugin methods:

type DeskWithPlugins = typeof desk & {
  activeId?: Ref<string | number | null>;
  getActive?: () => CheckInItem<PluginItemData> | null;
  getHistory?: () => Array<{ action: string; id: string | number; timestamp: number }>;
  setActive?: (id: string | number | null) => void;
};

History Entry Structure

Each history entry contains:

{
  action: 'check-in' | 'check-out' | 'update',
  id: string | number,
  data?: T,
  timestamp: number
}

How It Works

  1. Plugins are created before desk initialization
  2. Desk is created with plugins array
  3. Plugins extend the desk with additional methods and reactive properties
  4. Child components automatically check in when mounted
  5. History records all check-in, check-out, and update operations with timestamps
  6. Active item is tracked and updated when selection changes
  7. Real-time monitoring of all operations through reactive history

Usage

This pattern is ideal for:

  • Lists with selection tracking and operation logging
  • Activity monitoring and audit trails
  • Debugging component lifecycle issues
  • State management with operation history
  • Applications needing active item highlighting
  • Real-time operation dashboards
  • Modern Vue 3 projects using custom UI components for accessibility and design consistency