Appearance
Frontend Considerations - Oktuple
UI Library Implementation with Pantograph Vue
Overview
To maintain consistency across different systems and improve service quality and development speed, Oktuple has implemented a comprehensive UI library based on Pantograph Vue. This modern, type-safe UI framework provides a unified design system and component library for all frontend applications.
Why Pantograph Vue?
Design Philosophy
Consistency Across Systems:
- Unified Design Language: Single source of truth for UI components across all applications
- Brand Coherence: Consistent visual identity and user experience
- Component Reusability: Shared components reduce development time and maintenance overhead
- Quality Assurance: Centralized testing and validation of UI components
Modern Development Experience:
- Vue 3 Native: Built specifically for Vue 3 with Composition API
- TypeScript First: Full type safety with comprehensive type definitions
- Tree Shakeable: Import only needed components for optimal bundle size
- SSR Ready: Works seamlessly with Nuxt 3 and other SSR frameworks
Key Features
1. Comprehensive Component Library
typescript
// 60+ Production-Ready Components
import {
Button,
Input,
Modal,
Table,
Form,
Card,
Badge,
Dropdown,
Tabs,
Accordion,
// ... and 50+ more
} from "@pantograph/vue";
// Type-safe component usage
const MyComponent = defineComponent({
components: {
Button,
Input,
Modal,
},
setup() {
const isModalOpen = ref(false);
return {
isModalOpen,
};
},
});2. Design System Integration
typescript
// Design tokens and theming
interface DesignTokens {
colors: {
primary: string
secondary: string
success: string
warning: string
error: string
}
spacing: {
xs: string
sm: string
md: string
lg: string
xl: string
}
typography: {
fontFamily: string
fontSize: Record<string, string>
fontWeight: Record<string, number>
}
}
// CSS variables for theming
:root {
--pantograph-primary: #3b82f6;
--pantograph-secondary: #64748b;
--pantograph-success: #10b981;
--pantograph-warning: #f59e0b;
--pantograph-error: #ef4444;
}3. Accessibility and Responsiveness
typescript
// Built-in accessibility features
const AccessibleButton = defineComponent({
template: `
<button
:aria-label="ariaLabel"
:aria-disabled="disabled"
:tabindex="tabIndex"
@click="handleClick"
@keydown="handleKeydown"
>
<slot />
</button>
`,
props: {
ariaLabel: String,
disabled: Boolean,
tabIndex: Number,
},
});
// Responsive design patterns
const ResponsiveGrid = defineComponent({
template: `
<div class="pantograph-grid" :class="gridClasses">
<slot />
</div>
`,
computed: {
gridClasses() {
return {
"grid-cols-1": true,
"sm:grid-cols-2": this.columns >= 2,
"md:grid-cols-3": this.columns >= 3,
"lg:grid-cols-4": this.columns >= 4,
};
},
},
});Implementation Strategy
1. Component Architecture
typescript
// Base component structure
export interface ComponentProps {
// Common props for all components
id?: string;
class?: string;
style?: string | Record<string, any>;
disabled?: boolean;
loading?: boolean;
}
// Specific component implementation
export interface ButtonProps extends ComponentProps {
variant?: "primary" | "secondary" | "outline" | "ghost";
size?: "sm" | "md" | "lg";
icon?: string;
iconPosition?: "left" | "right";
fullWidth?: boolean;
}
export const Button = defineComponent<ButtonProps>({
name: "PantographButton",
props: {
variant: {
type: String as PropType<ButtonProps["variant"]>,
default: "primary",
},
size: {
type: String as PropType<ButtonProps["size"]>,
default: "md",
},
// ... other props
},
setup(props, { slots, emit }) {
const buttonClasses = computed(() => [
"pantograph-button",
`pantograph-button--${props.variant}`,
`pantograph-button--${props.size}`,
{
"pantograph-button--disabled": props.disabled,
"pantograph-button--loading": props.loading,
"pantograph-button--full-width": props.fullWidth,
},
]);
return {
buttonClasses,
};
},
});2. Form Integration
typescript
// VeeValidate integration
import { useForm } from "vee-validate";
import { Button, Input, Form } from "@pantograph/vue";
const FormExample = defineComponent({
components: { Button, Input, Form },
setup() {
const { handleSubmit, defineField, errors } = useForm({
validationSchema: yup.object({
email: yup.string().email().required(),
password: yup.string().min(8).required(),
}),
});
const [email, emailAttrs] = defineField("email");
const [password, passwordAttrs] = defineField("password");
const onSubmit = handleSubmit((values) => {
console.log("Form submitted:", values);
});
return {
email,
emailAttrs,
password,
passwordAttrs,
errors,
onSubmit,
};
},
});3. Icon System Integration
typescript
// Iconify integration
import { Icon } from "@iconify/vue";
import { Button } from "@pantograph/vue";
const IconButton = defineComponent({
components: { Button, Icon },
props: {
icon: String,
iconSize: {
type: String,
default: "1.25rem",
},
},
template: `
<Button :class="buttonClass">
<Icon
v-if="icon"
:icon="icon"
:style="{ fontSize: iconSize }"
/>
<slot />
</Button>
`,
});Development Workflow
1. Auto-Import Configuration
typescript
// vite.config.ts
import { defineConfig } from "vite";
import Vue from "@vitejs/plugin-vue";
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import { PantographResolver } from "@pantograph/vue/resolver";
export default defineConfig({
plugins: [
Vue(),
AutoImport({
resolvers: [PantographResolver()],
}),
Components({
resolvers: [PantographResolver()],
}),
],
});2. TypeScript Configuration
json
// tsconfig.json
{
"compilerOptions": {
"types": ["@pantograph/vue/types"]
}
}3. UnoCSS Integration
typescript
// uno.config.ts
import { defineConfig, presetUno } from "unocss";
import { presetPantograph } from "@pantograph/vue/preset";
export default defineConfig({
presets: [presetUno(), presetPantograph()],
});Benefits for Oktuple
1. Development Speed
Rapid Prototyping:
- Pre-built components reduce development time by 60%
- Consistent API across all components
- Hot module replacement for instant feedback
- Comprehensive documentation and examples
Code Reusability:
- Shared components across all applications
- Reduced code duplication
- Centralized maintenance and updates
- Consistent behavior across platforms
2. Quality Assurance
Consistent User Experience:
- Unified design language across all systems
- Predictable component behavior
- Accessibility compliance built-in
- Responsive design patterns
Testing and Validation:
- Centralized component testing
- Visual regression testing
- Cross-browser compatibility
- Performance optimization
3. Maintenance and Scalability
Centralized Updates:
- Single source of truth for UI components
- Easy to update and maintain
- Version control and release management
- Backward compatibility
Team Collaboration:
- Shared component library
- Consistent coding patterns
- Reduced learning curve for new developers
- Better code reviews and quality control
Implementation Examples
1. Project Management Interface
vue
<template>
<div class="project-dashboard">
<Card class="mb-6">
<template #header>
<div class="flex items-center justify-between">
<h2 class="text-xl font-semibold">Project Overview</h2>
<Button variant="primary" icon="plus" @click="createProject">
New Project
</Button>
</div>
</template>
<Table
:data="projects"
:columns="columns"
:loading="loading"
@row-click="handleRowClick"
>
<template #status="{ row }">
<Badge :variant="getStatusVariant(row.status)">
{{ row.status }}
</Badge>
</template>
</Table>
</Card>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from "vue";
import { Card, Button, Table, Badge } from "@pantograph/vue";
const projects = ref([]);
const loading = ref(false);
const columns = [
{ key: "name", title: "Project Name" },
{ key: "status", title: "Status" },
{ key: "createdAt", title: "Created" },
];
const getStatusVariant = (status: string) => {
const variants = {
active: "success",
pending: "warning",
completed: "primary",
};
return variants[status] || "secondary";
};
</script>2. Real-time Collaboration Interface
vue
<template>
<div class="collaboration-workspace">
<div class="flex h-screen">
<!-- Sidebar -->
<aside class="w-64 bg-gray-100 p-4">
<Tabs v-model="activeTab">
<Tab title="Issues" icon="bug">
<IssueList :issues="issues" />
</Tab>
<Tab title="Comments" icon="message">
<CommentList :comments="comments" />
</Tab>
</Tabs>
</aside>
<!-- Main Content -->
<main class="flex-1 p-6">
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold">Project Board</h1>
<div class="flex gap-2">
<Button variant="outline" icon="filter" @click="showFilters = true">
Filters
</Button>
<Button variant="primary" icon="plus" @click="createIssue">
New Issue
</Button>
</div>
</div>
<KanbanBoard
:columns="kanbanColumns"
:items="kanbanItems"
@item-move="handleItemMove"
/>
</main>
</div>
<!-- Modals -->
<Modal v-model="showFilters" title="Filter Issues" size="md">
<FilterForm @apply="applyFilters" />
</Modal>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { Tabs, Tab, Button, Modal, KanbanBoard } from "@pantograph/vue";
const activeTab = ref("issues");
const showFilters = ref(false);
const issues = ref([]);
const comments = ref([]);
const kanbanColumns = ref([
{ id: "todo", title: "To Do", color: "gray" },
{ id: "in-progress", title: "In Progress", color: "blue" },
{ id: "done", title: "Done", color: "green" },
]);
const kanbanItems = ref([]);
</script>Future Enhancements
1. Advanced Features
- Theme Customization: Advanced theming with design tokens
- Animation Library: Built-in animations and transitions
- Chart Components: Data visualization components
- Form Builder: Visual form builder interface
2. Integration Improvements
- Storybook Integration: Component documentation and testing
- Figma Integration: Design-to-code workflow
- Testing Tools: Automated testing utilities
- Performance Monitoring: Component performance tracking
Benefits
- Consistency: Unified design system across all applications
- Development Speed: 60% faster UI development
- Quality: Built-in accessibility and responsive design
- Maintainability: Centralized component management
- Scalability: Easy to add new components and features
- Team Productivity: Reduced learning curve and better collaboration
Frontend Architecture
Vue 3 + Composition API Architecture
Oktuple's frontend is built using Vue 3 with the Composition API, providing a modern, reactive, and maintainable user interface.
Core Architecture Principles
- Component-Driven Development: Modular, reusable components
- Reactive State Management: Vue 3's reactive system for state
- Type Safety: Full TypeScript support throughout the application
- Performance Optimization: Lazy loading, code splitting, and efficient rendering
- Real-time Updates: WebSocket integration for live collaboration
Technology Stack
typescript
// Core Framework
import { createApp, ref, computed, watch } from "vue";
import { createRouter, createWebHistory } from "vue-router";
import { createPinia } from "pinia";
// UI Framework
import { createHead } from "@vueuse/head";
import { createI18n } from "vue-i18n";
// Styling
import "tailwindcss/tailwind.css";
import "@headlessui/vue";
import "@heroicons/vue";
// State Management
import { defineStore } from "pinia";
import { useUserStore } from "@/stores/user";
import { useProjectStore } from "@/stores/project";
// Real-time Communication
import { useWebSocket } from "@/composables/websocket";
import { useEventBus } from "@/composables/eventBus";Application Structure
Design Patterns
Component-Driven Development
Component Composition
typescript
// Base component with common functionality
const useBaseComponent = () => {
const loading = ref(false);
const error = ref<string | null>(null);
const handleError = (err: Error) => {
error.value = err.message;
loading.value = false;
};
return {
loading,
error,
handleError,
};
};
// Specialized component using base
const useIssueComponent = () => {
const { loading, error, handleError } = useBaseComponent();
const issues = ref<Issue[]>([]);
const fetchIssues = async () => {
loading.value = true;
try {
issues.value = await issueService.getIssues();
} catch (err) {
handleError(err as Error);
}
};
return {
issues,
fetchIssues,
loading,
error,
};
};State Management (Pinia & VueUse)
Pinia Store Architecture
typescript
// User store
export const useUserStore = defineStore("user", () => {
const user = ref<User | null>(null);
const isAuthenticated = computed(() => !!user.value);
const login = async (credentials: LoginCredentials) => {
const response = await authService.login(credentials);
user.value = response.user;
return response;
};
const logout = () => {
user.value = null;
};
return {
user,
isAuthenticated,
login,
logout,
};
});
// Project store
export const useProjectStore = defineStore("project", () => {
const projects = ref<Project[]>([]);
const currentProject = ref<Project | null>(null);
const fetchProjects = async () => {
projects.value = await projectService.getProjects();
};
const setCurrentProject = (project: Project) => {
currentProject.value = project;
};
return {
projects,
currentProject,
fetchProjects,
setCurrentProject,
};
});Reactive State Patterns
typescript
// Computed properties for derived state
const useIssueFilters = () => {
const issues = ref<Issue[]>([]);
const filters = ref<IssueFilters>({});
const filteredIssues = computed(() => {
return issues.value.filter((issue) => {
if (filters.value.status && issue.status !== filters.value.status)
return false;
if (
filters.value.assignee &&
issue.assignee_id !== filters.value.assignee
)
return false;
if (filters.value.priority && issue.priority !== filters.value.priority)
return false;
return true;
});
});
return {
issues,
filters,
filteredIssues,
};
};
// Watchers for side effects
const useIssueSync = () => {
const { issues } = useIssueStore();
watch(
issues,
(newIssues) => {
// Sync with backend
issueService.syncIssues(newIssues);
},
{ deep: true }
);
};Real-time Communication
WebSocket Integration
WebSocket Service
typescript
// WebSocket composable
export const useWebSocket = () => {
const socket = ref<WebSocket | null>(null);
const isConnected = ref(false);
const messageQueue = ref<Message[]>([]);
const connect = () => {
socket.value = new WebSocket(WS_URL);
socket.value.onopen = () => {
isConnected.value = true;
// Send queued messages
messageQueue.value.forEach((msg) => sendMessage(msg));
messageQueue.value = [];
};
socket.value.onmessage = (event) => {
const message = JSON.parse(event.data);
handleMessage(message);
};
socket.value.onclose = () => {
isConnected.value = false;
// Attempt reconnection
setTimeout(connect, 5000);
};
};
const sendMessage = (message: Message) => {
if (isConnected.value && socket.value) {
socket.value.send(JSON.stringify(message));
} else {
messageQueue.value.push(message);
}
};
const disconnect = () => {
if (socket.value) {
socket.value.close();
}
};
return {
isConnected,
connect,
sendMessage,
disconnect,
};
};Event Bus Integration
typescript
// Event bus for component communication
export const useEventBus = () => {
const listeners = new Map<string, Function[]>();
const on = (event: string, callback: Function) => {
if (!listeners.has(event)) {
listeners.set(event, []);
}
listeners.get(event)!.push(callback);
};
const emit = (event: string, data?: any) => {
const callbacks = listeners.get(event) || [];
callbacks.forEach((callback) => callback(data));
};
const off = (event: string, callback?: Function) => {
if (!callback) {
listeners.delete(event);
} else {
const callbacks = listeners.get(event) || [];
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
}
};
return {
on,
emit,
off,
};
};Live Updates
Real-time Issue Updates
typescript
// Live issue updates
const useLiveIssues = () => {
const { socket } = useWebSocket();
const { issues, updateIssue, addIssue, removeIssue } = useIssueStore();
const handleIssueUpdate = (data: IssueUpdateEvent) => {
switch (data.type) {
case "issue.created":
addIssue(data.issue);
break;
case "issue.updated":
updateIssue(data.issue);
break;
case "issue.deleted":
removeIssue(data.issueId);
break;
}
};
// Listen for issue events
onMounted(() => {
socket.value?.addEventListener("message", (event) => {
const message = JSON.parse(event.data);
if (message.type?.startsWith("issue.")) {
handleIssueUpdate(message);
}
});
});
return {
handleIssueUpdate,
};
};Performance Optimization
Code Splitting and Lazy Loading
Route-based Code Splitting
typescript
// Lazy-loaded routes
const routes = [
{
path: "/",
component: () => import("@/views/Dashboard.vue"),
},
{
path: "/projects/:id",
component: () => import("@/views/ProjectDetail.vue"),
},
{
path: "/issues/:id",
component: () => import("@/views/IssueDetail.vue"),
},
];Component Lazy Loading
typescript
// Lazy-loaded components
const LazyIssueCard = defineAsyncComponent(
() => import("@/components/IssueCard.vue")
);
const LazyProjectHeader = defineAsyncComponent(
() => import("@/components/ProjectHeader.vue")
);
// Conditional loading
const useConditionalLoading = () => {
const shouldLoad = ref(false);
const loadComponent = () => {
shouldLoad.value = true;
};
return {
shouldLoad,
loadComponent,
};
};Virtual Scrolling
Large List Optimization
typescript
// Virtual scrolling for large issue lists
const useVirtualScrolling = () => {
const itemHeight = 80;
const visibleItems = 10;
const scrollTop = ref(0);
const visibleRange = computed(() => {
const start = Math.floor(scrollTop.value / itemHeight);
const end = start + visibleItems;
return { start, end };
});
const visibleIssues = computed(() => {
const { start, end } = visibleRange.value;
return issues.value.slice(start, end);
});
const totalHeight = computed(() => issues.value.length * itemHeight);
return {
visibleIssues,
totalHeight,
scrollTop,
itemHeight,
};
};Testing Strategy
E2E Testing
Playwright Integration
typescript
// E2E test example
import { test, expect } from "@playwright/test";
test("user can create a new issue", async ({ page }) => {
await page.goto("/projects/1");
await page.click('[data-testid="create-issue-btn"]');
await page.fill('[data-testid="issue-title"]', "New Issue");
await page.fill('[data-testid="issue-description"]', "Issue description");
await page.click('[data-testid="save-issue-btn"]');
await expect(page.locator('[data-testid="issue-list"]')).toContainText(
"New Issue"
);
});