Adding a Standalone Page
Create the Vue component
Add a new .vue file in pkg/pastures/pages/. Use the standard template:<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { engineFetch, isGlobalDemoMode, safeJson } from '../lib/pasturesApi';
const loading = ref(true);
const data = ref<any>(null);
const DEMO_DATA = {
items: [
{ id: '1', name: 'Example item', status: 'healthy' },
],
};
onMounted(async () => {
if (isGlobalDemoMode()) {
await new Promise((r) => setTimeout(r, 400));
data.value = DEMO_DATA;
loading.value = false;
return;
}
const res = await engineFetch('/api/my-feature');
data.value = safeJson(await res.json());
loading.value = false;
});
</script>
<template>
<div class="pastures-page">
<h1>My Feature</h1>
<div v-if="loading" class="loading-indicator">Loading…</div>
<div v-else>
<!-- render data -->
</div>
</div>
</template>
Register the route in index.ts
Import the component and add a route entry inside the plugin() function:import MyFeaturePage from './pages/MyFeaturePage.vue';
plugin.addRoute({
name: 'pastures-my-feature',
path: '/pastures/my-feature',
component: MyFeaturePage,
meta: { product: 'pastures' },
});
Add a virtualType in product.ts
Register the sidebar entry:virtualType({
label: 'My Feature',
name: 'pastures-my-feature',
route: { name: 'pastures-my-feature' },
namespaced: false,
});
Add to basicType array
Include the new type in the appropriate basicType() call so it appears in the sidebar section:basicType([
// ... existing entries
'pastures-my-feature',
]);
Set sidebar position with weightType
Control where the entry appears in the sidebar. Higher weight = higher position.weightType('pastures-my-feature', 50);
Add demo data
If your page calls engineFetch, add a path match in lib/demoResponses.ts:case '/api/my-feature':
return {
items: [
{ id: '1', name: 'Example item', status: 'healthy' },
],
};
If your page uses inline fetch, define a DEMO_* constant in the component and check isGlobalDemoMode() in onMounted (as shown in step 1).
Adding a Tab to an Existing Tabbed Page
Tabbed container pages (OperationsPage, HarvesterPage, InfrastructurePage, AIAgentsPage) render child components as tabs.
Create the tab component
Create your .vue file in pages/ as above, but add support for the embedded prop:<script setup lang="ts">
defineProps<{
embedded?: boolean;
}>();
</script>
<template>
<div class="pastures-page">
<h1 v-if="!embedded">My Tab Feature</h1>
<!-- page content -->
</div>
</template>
When embedded is true, the component hides its own header since the parent tabbed page provides the tab bar. Import and add to the tabs array
In the parent tabbed page (e.g., OperationsPage.vue), import the new component and add it to the tabs configuration:import MyTabFeature from './MyTabFeaturePage.vue';
const tabs = [
// ... existing tabs
{ label: 'My Tab', component: MyTabFeature },
];
Add demo data
Follow the same demo data pattern described above for standalone pages.
You do not need to register a separate route or sidebar entry for tabs — the parent container page handles routing.