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:
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
Directory Purpose 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
Generates typed GraphQL clients from 3 schemas:
Output Directory Schema Source src/networks/common/graphqlApi/gql/../api/.checkpoint/schema.gqlsrc/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