Skip to main content

Overview

The UI application is a modern Vue 3 frontend that provides a unified interface for interacting with both onchain governance protocols (Snapshot X, Compound Governor, OpenZeppelin Governor) and offchain Snapshot spaces.
The UI communicates with the API service via GraphQL for data queries and Mana via JSON-RPC for gasless transaction submission.

Quick Start

Development Server

Start the Vite development server on port 8080:
yarn dev
The application will be available at http://localhost:8080.

Other Commands

yarn build          # Production build
yarn codegen        # Generate GraphQL types
yarn typecheck      # TypeScript type checking
yarn test           # Run tests
yarn test:watch     # Watch mode testing
yarn storybook      # Launch Storybook on port 6006

Technology Stack

Vue 3

Composition API with <script setup> syntax

Vite

Lightning-fast dev server and build tool (rolldown-vite 7.3.1)

Pinia

State management for client-side state

TanStack Query

Server state management (@tanstack/vue-query 5.64.2)

Vue Router

Client-side routing (v4.2.5)

TailwindCSS

Utility-first styling with custom design system

Additional Libraries

  • @snapshot-labs/sx - Governance SDK
  • @apollo/client - GraphQL client
  • Ethers.js v6 - Ethereum interactions
  • Starknet.js 7.6.4 - Starknet interactions
  • Reown AppKit - Wallet connections
  • Tiptap - Rich text editor
  • VueUse - Vue composition utilities

Architecture

Data Flow

Onchain Spaces:
  • Fetch data from API (GraphQL)
  • Submit transactions via Mana (JSON-RPC) for gasless voting or directly to networks
Offchain Spaces:
  • Fetch data from snapshot-hub (GraphQL)
  • Submit signed messages to snapshot-sequencer (JSON-RPC)

Auto-Imports

Do not manually import Vue APIs, composables, stores, or components - they are auto-imported via vite.config.ts.
Auto-imported APIs:
// Vue APIs (all vue exports)
ref, computed, watch, onMounted, reactive, nextTick

// Vue Router (all vue-router exports)
useRoute, useRouter, onBeforeRouteLeave

// VueUse (all @vueuse/core exports)
useEventListener, useLocalStorage, useClipboard

// Composables (src/composables/)
useWeb3(), useModal(), useActions(), useBalances()

// Stores (src/stores/)
useUiStore(), useContactsStore(), useMetaStore()
Component Auto-Import: Components use directory-as-namespace pattern:
<!-- src/components/Ui/Button.vue -->
<UiButton />

<!-- src/components/Modal/Vote.vue -->
<ModalVote />

<!-- src/components/App/TopNavigation.vue -->
<AppTopNavigation />
Icon Auto-Resolution:
<!-- Heroicons Outline -->
<IH-search />

<!-- Heroicons Solid -->
<IS-check />

<!-- Custom icons from src/assets/icons/ -->
<IC-snapshot />

Component Structure

All components use TypeScript setup syntax:
<script lang="ts" setup>
const props = defineProps<{
  title: string;
  count?: number;
}>();

const emit = defineEmits<{
  (e: 'submit', value: string): void;
  (e: 'close'): void;
}>();

const handleSubmit = () => {
  emit('submit', 'value');
};
</script>

<template>
  <div class="skin-bg">
    <h1 class="skin-heading">{{ title }}</h1>
    <UiButton @click="handleSubmit">Submit</UiButton>
  </div>
</template>

Component Directories

DirectoryPurpose
Ui/Base reusable components (Button, Modal, Form, etc.)
Modal/Specialized modal components
App/App-level components (navigation, sidebars)
Layout/Page layout wrappers
Site/Site-wide elements
Landing/Landing page components
Widget/Embeddable widget components

State Management

Pinia Stores

For client-side state (UI preferences, contacts, followed spaces):
// src/stores/ui.ts
export const useUiStore = defineStore('ui', {
  state: () => ({
    sidebarOpen: false
  }),
  actions: {
    toggleSidebar() {
      this.sidebarOpen = !this.sidebarOpen;
    }
  }
});

// Usage (auto-imported)
const ui = useUiStore();
ui.toggleSidebar();

Vue Query

For server state with reactive query keys:
// src/queries/proposals.ts
import { useQuery } from '@tanstack/vue-query';
import type { MaybeRefOrGetter } from 'vue';

export function useProposalsQuery(spaceId: MaybeRefOrGetter<string>) {
  return useQuery({
    queryKey: computed(() => ['proposals', toValue(spaceId), 'list']),
    queryFn: async () => {
      // Fetch proposals
    }
  });
}
Always handle isPending, isError, and data states in templates when using Vue Query.

Styling with Tailwind

Custom Design System

The UI uses custom Tailwind scales - not default Tailwind values!
Spacing Scale (non-standard):
  • 0 = 0px, 0.5 = 2px, 1 = 4px, 1.5 = 6px
  • 2 = 8px, 2.5 = 12px, 3 = 16px, 3.5 = 20px
  • 4 = 24px, 5 = 32px, 6 = 40px, 7 = 48px
  • 8 = 64px, 9 = 72px, 10 = 80px, 11 = 88px, 12 = 96px
Font Sizes:
  • xs = 14px, sm = 16px, base = 18px
  • md = 20px, lg = 22px, xl = 28px
Breakpoints:
  • minimum = 320px, xs = 420px, sm = 544px
  • md = 768px, lg = 1012px, xl = 1280px
  • 2xl = 1536px, 3xl = 1844px, maximum = 1900px

Theme Colors

Always use skin-* classes for theming - no hardcoded colors!
<!-- Backgrounds -->
<div class="skin-bg border skin-border">
  
  <!-- Text -->
  <h1 class="skin-heading">Title</h1>
  <p class="skin-text">Body text</p>
  <a class="skin-link">Link</a>
  
  <!-- Accents -->
  <button class="skin-primary hover:skin-accent-hover">
    Primary Action
  </button>
  
  <button class="skin-danger hover:skin-danger-hover">
    Delete
  </button>
</div>

GraphQL Code Generation

yarn codegen
Generates typed GraphQL clients from 3 schemas:
Output DirectorySchema Source
src/networks/common/graphqlApi/gql/../api/.checkpoint/schema.gql
src/helpers/auction/gql/Subgraph endpoint
src/helpers/auction/referral/gql/Brokester API
Never edit generated gql/ directories - they are gitignored and regenerated on yarn codegen.

File Structure

src/
  assets/        # Static assets and custom icons
  components/    # Vue components (auto-imported)
  composables/   # Composition functions (auto-imported)
  docs/          # In-app documentation
  helpers/       # Pure utility functions
  networks/      # Network implementations (EVM, Starknet, Offchain)
  queries/       # Vue Query composables
  routes/        # Vue Router definitions
  stores/        # Pinia stores (auto-imported)
  views/         # Page-level views

Testing

  • Vitest for unit tests with happy-dom
  • @vue/test-utils for component testing
  • Storybook 9 for component documentation
yarn test           # Run tests once
yarn test:watch     # Watch mode
yarn storybook      # Launch Storybook

Path Alias

import { helper } from '@/helpers/format';
// Resolves to ./src/helpers/format
Configured in vite.config.ts via resolve.alias.

Electron Support

The UI can run as a desktop application:
yarn dev:electron     # Development mode
yarn build:electron   # Build for multiple platforms

Key Dependencies

From package.json:
{
  "dependencies": {
    "vue": "^3.4.15",
    "vue-router": "^4.2.5",
    "pinia": "^2.1.6",
    "@tanstack/vue-query": "^5.64.2",
    "@snapshot-labs/sx": "^0.1.0",
    "@apollo/client": "^3.12.9",
    "ethers": "^6.13.5",
    "starknet": "7.6.4",
    "@reown/appkit": "^1.8.8",
    "@vueuse/core": "^10.4.1"
  }
}

Best Practices

  • Use <script lang="ts" setup> for all components
  • Use skin-* color classes for theming
  • Put queries in src/queries/ using Vue Query patterns
  • Use custom Tailwind spacing scale
  • Don’t manually import auto-imported APIs
  • Don’t edit gql/ directories

Build docs developers (and LLMs) love