A comprehensive example demonstrating the use of multiple plugins with modern UI components and improved UX:
activeItemPluginhistoryPluginTracks all check-ins, check-outs, and updates. Most recent operations appear first.
plugin-stack/
├── index.ts
├── PluginStack.vue
├── PluginStackListItem.vue
├── PluginStackActiveItemPanel.vue
└── PluginStackHistoryPanel.vue
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';
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>
<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>
<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>
<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>
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],
});
Tracks the currently selected item:
setActive(id): Set the active itemgetActive(): Get the active itemactiveId: Reactive reference to the active IDMaintains a log of all desk operations:
getHistory(): Get the complete operation historygetLastHistory(count): Get the last N operationsgetHistoryByAction(action): Filter history by action typeclearHistory(): Clear all history entrieshistory: Reactive reference to the history arrayUse 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;
};
Each history entry contains:
{
action: 'check-in' | 'check-out' | 'update',
id: string | number,
data?: T,
timestamp: number
}
This pattern is ideal for: