Skip to main content

Adding a Standalone Page

1

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>
2

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' },
});
3

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,
});
4

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',
]);
5

Set sidebar position with weightType

Control where the entry appears in the sidebar. Higher weight = higher position.
weightType('pastures-my-feature', 50);
6

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.
1

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.
2

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 },
];
3

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.