Skip to content

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"
  );
});