Table of Contents

Design System

Consistent Design Language & Component Library

Jankx 2.0 cung cấp design system hoàn chỉnh với component library, design tokens và design-to-code workflow.

🎨 Design System Architecture

Design System Structure

┌─────────────────────────────────────┐
│         Design System              │
│  ┌─────────────┐  ┌─────────────┐  │
│  │   Design    │  │   Component │  │
│  │   Tokens    │  │   Library   │  │
│  └─────────────┘  └─────────────┘  │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│         Design Tools               │
│  ┌─────────────┐  ┌─────────────┐  │
│  │   Figma     │  │   Storybook │  │
│  │   Plugin    │  │   Integration│  │
│  └─────────────┘  └─────────────┘  │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│         Code Generation            │
│  ┌─────────────┐  ┌─────────────┐  │
│  │   CSS       │  │   Gutenberg │  │
│  │   Variables │  │   Blocks    │  │
│  └─────────────┘  └─────────────┘  │
└─────────────────────────────────────┘

🎯 Design Tokens

Color System

// assets/css/design-tokens/colors.scss
:root {
  // Primary Colors
  --color-primary-50: #eff6ff;
  --color-primary-100: #dbeafe;
  --color-primary-200: #bfdbfe;
  --color-primary-300: #93c5fd;
  --color-primary-400: #60a5fa;
  --color-primary-500: #3b82f6;
  --color-primary-600: #2563eb;
  --color-primary-700: #1d4ed8;
  --color-primary-800: #1e40af;
  --color-primary-900: #1e3a8a;

  // Secondary Colors
  --color-secondary-50: #f8fafc;
  --color-secondary-100: #f1f5f9;
  --color-secondary-200: #e2e8f0;
  --color-secondary-300: #cbd5e1;
  --color-secondary-400: #94a3b8;
  --color-secondary-500: #64748b;
  --color-secondary-600: #475569;
  --color-secondary-700: #334155;
  --color-secondary-800: #1e293b;
  --color-secondary-900: #0f172a;

  // Semantic Colors
  --color-success: #10b981;
  --color-warning: #f59e0b;
  --color-error: #ef4444;
  --color-info: #3b82f6;

  // Neutral Colors
  --color-white: #ffffff;
  --color-black: #000000;
  --color-gray-50: #f9fafb;
  --color-gray-100: #f3f4f6;
  --color-gray-200: #e5e7eb;
  --color-gray-300: #d1d5db;
  --color-gray-400: #9ca3af;
  --color-gray-500: #6b7280;
  --color-gray-600: #4b5563;
  --color-gray-700: #374151;
  --color-gray-800: #1f2937;
  --color-gray-900: #111827;
}

Typography System

// assets/css/design-tokens/typography.scss
:root {
  // Font Families
  --font-family-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  --font-family-serif: 'Georgia', 'Times New Roman', serif;
  --font-family-mono: 'JetBrains Mono', 'Fira Code', monospace;

  // Font Sizes
  --font-size-xs: 0.75rem;    // 12px
  --font-size-sm: 0.875rem;   // 14px
  --font-size-base: 1rem;     // 16px
  --font-size-lg: 1.125rem;   // 18px
  --font-size-xl: 1.25rem;    // 20px
  --font-size-2xl: 1.5rem;    // 24px
  --font-size-3xl: 1.875rem;  // 30px
  --font-size-4xl: 2.25rem;   // 36px
  --font-size-5xl: 3rem;      // 48px
  --font-size-6xl: 3.75rem;   // 60px

  // Font Weights
  --font-weight-thin: 100;
  --font-weight-light: 300;
  --font-weight-normal: 400;
  --font-weight-medium: 500;
  --font-weight-semibold: 600;
  --font-weight-bold: 700;
  --font-weight-extrabold: 800;
  --font-weight-black: 900;

  // Line Heights
  --line-height-tight: 1.25;
  --line-height-snug: 1.375;
  --line-height-normal: 1.5;
  --line-height-relaxed: 1.625;
  --line-height-loose: 2;

  // Letter Spacing
  --letter-spacing-tighter: -0.05em;
  --letter-spacing-tight: -0.025em;
  --letter-spacing-normal: 0em;
  --letter-spacing-wide: 0.025em;
  --letter-spacing-wider: 0.05em;
  --letter-spacing-widest: 0.1em;
}

Spacing System

// assets/css/design-tokens/spacing.scss
:root {
  // Spacing Scale
  --spacing-0: 0;
  --spacing-1: 0.25rem;   // 4px
  --spacing-2: 0.5rem;    // 8px
  --spacing-3: 0.75rem;   // 12px
  --spacing-4: 1rem;      // 16px
  --spacing-5: 1.25rem;   // 20px
  --spacing-6: 1.5rem;    // 24px
  --spacing-8: 2rem;      // 32px
  --spacing-10: 2.5rem;   // 40px
  --spacing-12: 3rem;     // 48px
  --spacing-16: 4rem;     // 64px
  --spacing-20: 5rem;     // 80px
  --spacing-24: 6rem;     // 96px
  --spacing-32: 8rem;     // 128px
  --spacing-40: 10rem;    // 160px
  --spacing-48: 12rem;    // 192px
  --spacing-56: 14rem;    // 224px
  --spacing-64: 16rem;    // 256px

  // Container Max Widths
  --container-sm: 640px;
  --container-md: 768px;
  --container-lg: 1024px;
  --container-xl: 1280px;
  --container-2xl: 1536px;

  // Border Radius
  --radius-none: 0;
  --radius-sm: 0.125rem;   // 2px
  --radius-base: 0.25rem;  // 4px
  --radius-md: 0.375rem;   // 6px
  --radius-lg: 0.5rem;     // 8px
  --radius-xl: 0.75rem;    // 12px
  --radius-2xl: 1rem;      // 16px
  --radius-3xl: 1.5rem;    // 24px
  --radius-full: 9999px;

  // Shadows
  --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
  --shadow-base: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
  --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
  --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
  --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
  --shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25);
}

🧩 Component Library

Button Component

// assets/css/components/button.scss
.jankx-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: var(--spacing-3) var(--spacing-6);
  font-size: var(--font-size-base);
  font-weight: var(--font-weight-medium);
  line-height: var(--line-height-normal);
  border-radius: var(--radius-md);
  border: 1px solid transparent;
  cursor: pointer;
  transition: all 0.2s ease-in-out;
  text-decoration: none;

  &:focus {
    outline: 2px solid var(--color-primary-500);
    outline-offset: 2px;
  }

  &:disabled {
    opacity: 0.5;
    cursor: not-allowed;
  }

  // Variants
  &--primary {
    background-color: var(--color-primary-600);
    color: var(--color-white);

    &:hover {
      background-color: var(--color-primary-700);
    }

    &:active {
      background-color: var(--color-primary-800);
    }
  }

  &--secondary {
    background-color: var(--color-white);
    color: var(--color-gray-700);
    border-color: var(--color-gray-300);

    &:hover {
      background-color: var(--color-gray-50);
      border-color: var(--color-gray-400);
    }
  }

  &--outline {
    background-color: transparent;
    color: var(--color-primary-600);
    border-color: var(--color-primary-600);

    &:hover {
      background-color: var(--color-primary-50);
    }
  }

  // Sizes
  &--sm {
    padding: var(--spacing-2) var(--spacing-4);
    font-size: var(--font-size-sm);
  }

  &--lg {
    padding: var(--spacing-4) var(--spacing-8);
    font-size: var(--font-size-lg);
  }

  // States
  &--loading {
    position: relative;
    color: transparent;

    &::after {
      content: '';
      position: absolute;
      width: 1rem;
      height: 1rem;
      border: 2px solid currentColor;
      border-radius: 50%;
      border-top-color: transparent;
      animation: spin 1s linear infinite;
    }
  }
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

Card Component

// assets/css/components/card.scss
.jankx-card {
  background-color: var(--color-white);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-base);
  overflow: hidden;
  transition: box-shadow 0.2s ease-in-out;

  &:hover {
    box-shadow: var(--shadow-lg);
  }

  // Card Header
  &__header {
    padding: var(--spacing-6);
    border-bottom: 1px solid var(--color-gray-200);
  }

  &__title {
    font-size: var(--font-size-xl);
    font-weight: var(--font-weight-semibold);
    color: var(--color-gray-900);
    margin: 0 0 var(--spacing-2) 0;
  }

  &__subtitle {
    font-size: var(--font-size-sm);
    color: var(--color-gray-600);
    margin: 0;
  }

  // Card Body
  &__body {
    padding: var(--spacing-6);
  }

  &__content {
    color: var(--color-gray-700);
    line-height: var(--line-height-relaxed);
  }

  // Card Footer
  &__footer {
    padding: var(--spacing-6);
    border-top: 1px solid var(--color-gray-200);
    background-color: var(--color-gray-50);
  }

  // Card Actions
  &__actions {
    display: flex;
    gap: var(--spacing-3);
    justify-content: flex-end;
  }

  // Variants
  &--elevated {
    box-shadow: var(--shadow-lg);
  }

  &--outlined {
    box-shadow: none;
    border: 1px solid var(--color-gray-200);
  }

  &--interactive {
    cursor: pointer;

    &:hover {
      transform: translateY(-2px);
    }
  }
}

Form Components

// assets/css/components/form.scss
.jankx-form {
  &__group {
    margin-bottom: var(--spacing-4);
  }

  &__label {
    display: block;
    font-size: var(--font-size-sm);
    font-weight: var(--font-weight-medium);
    color: var(--color-gray-700);
    margin-bottom: var(--spacing-2);
  }

  &__input {
    width: 100%;
    padding: var(--spacing-3);
    font-size: var(--font-size-base);
    border: 1px solid var(--color-gray-300);
    border-radius: var(--radius-md);
    background-color: var(--color-white);
    transition: border-color 0.2s ease-in-out;

    &:focus {
      outline: none;
      border-color: var(--color-primary-500);
      box-shadow: 0 0 0 3px rgb(59 130 246 / 0.1);
    }

    &:invalid {
      border-color: var(--color-error);
    }

    &::placeholder {
      color: var(--color-gray-400);
    }
  }

  &__textarea {
    @extend .jankx-form__input;
    resize: vertical;
    min-height: 100px;
  }

  &__select {
    @extend .jankx-form__input;
    background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e");
    background-position: right var(--spacing-2) center;
    background-repeat: no-repeat;
    background-size: 1.5em 1.5em;
    padding-right: var(--spacing-10);
    appearance: none;
  }

  &__checkbox {
    display: flex;
    align-items: center;
    gap: var(--spacing-2);

    input[type="checkbox"] {
      width: 1rem;
      height: 1rem;
      border: 1px solid var(--color-gray-300);
      border-radius: var(--radius-sm);
      background-color: var(--color-white);

      &:checked {
        background-color: var(--color-primary-600);
        border-color: var(--color-primary-600);
      }
    }
  }

  &__radio {
    display: flex;
    align-items: center;
    gap: var(--spacing-2);

    input[type="radio"] {
      width: 1rem;
      height: 1rem;
      border: 1px solid var(--color-gray-300);
      border-radius: 50%;
      background-color: var(--color-white);

      &:checked {
        background-color: var(--color-primary-600);
        border-color: var(--color-primary-600);
      }
    }
  }

  &__error {
    font-size: var(--font-size-sm);
    color: var(--color-error);
    margin-top: var(--spacing-1);
  }

  &__help {
    font-size: var(--font-size-sm);
    color: var(--color-gray-600);
    margin-top: var(--spacing-1);
  }
}

🎨 Design Token Generator

Design Token Generator Implementation

<?php
namespace Jankx\Designer\Generator;

class DesignTokenGenerator
{
    private $designData;
    private $outputPath;

    public function __construct(DesignData $designData, string $outputPath)
    {
        $this->designData = $designData;
        $this->outputPath = $outputPath;
    }

    public function generateTokens(): void
    {
        $this->generateColorTokens();
        $this->generateTypographyTokens();
        $this->generateSpacingTokens();
        $this->generateComponentTokens();
    }

    private function generateColorTokens(): void
    {
        $colors = $this->designData->getColors();
        $scss = ":root {\n";

        foreach ($colors as $name => $color) {
            $scss .= "  --color-{$name}: {$color['value']};\n";
        }

        $scss .= "}\n";

        file_put_contents($this->outputPath . '/assets/css/tokens/colors.scss', $scss);
    }

    private function generateTypographyTokens(): void
    {
        $typography = $this->designData->getTypography();
        $scss = ":root {\n";

        foreach ($typography as $name => $font) {
            $scss .= "  --font-{$name}-family: {$font['font_family']};\n";
            $scss .= "  --font-{$name}-size: {$font['font_size']}px;\n";
            $scss .= "  --font-{$name}-weight: {$font['font_weight']};\n";
            $scss .= "  --font-{$name}-line-height: {$font['line_height']}px;\n";
        }

        $scss .= "}\n";

        file_put_contents($this->outputPath . '/assets/css/tokens/typography.scss', $scss);
    }

    private function generateSpacingTokens(): void
    {
        $spacing = $this->designData->getSpacing();
        $scss = ":root {\n";

        foreach ($spacing as $name => $space) {
            $scss .= "  --spacing-{$name}: {$space['value']};\n";
        }

        $scss .= "}\n";

        file_put_contents($this->outputPath . '/assets/css/tokens/spacing.scss', $scss);
    }

    private function generateComponentTokens(): void
    {
        $components = $this->designData->getComponents();
        $scss = "";

        foreach ($components as $name => $component) {
            $scss .= $this->generateComponentCSS($name, $component);
        }

        file_put_contents($this->outputPath . '/assets/css/components.scss', $scss);
    }

    private function generateComponentCSS(string $name, array $component): string
    {
        $css = ".jankx-{$name} {\n";

        if (isset($component['styles'])) {
            foreach ($component['styles'] as $property => $value) {
                $css .= "  {$property}: {$value};\n";
            }
        }

        $css .= "}\n\n";

        return $css;
    }
}

🎯 Storybook Integration

Storybook Configuration

// .storybook/main.js
module.exports = {
  stories: [
    '../stories/**/*.stories.mdx',
    '../stories/**/*.stories.@(js|jsx|ts|tsx)'
  ],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
    '@storybook/addon-a11y',
    '@storybook/addon-designs'
  ],
  framework: {
    name: '@storybook/html-webpack5',
    options: {}
  },
  docs: {
    autodocs: 'tag'
  }
};

Component Story Example

// stories/Button.stories.js
export default {
  title: 'Components/Button',
  component: 'jankx-button',
  parameters: {
    docs: {
      description: {
        component: 'A versatile button component with multiple variants and sizes.'
      }
    }
  },
  argTypes: {
    variant: {
      control: { type: 'select' },
      options: ['primary', 'secondary', 'outline'],
      description: 'Button variant'
    },
    size: {
      control: { type: 'select' },
      options: ['sm', 'md', 'lg'],
      description: 'Button size'
    },
    disabled: {
      control: { type: 'boolean' },
      description: 'Disabled state'
    },
    loading: {
      control: { type: 'boolean' },
      description: 'Loading state'
    }
  }
};

export const Primary = {
  args: {
    variant: 'primary',
    size: 'md',
    disabled: false,
    loading: false,
    children: 'Primary Button'
  }
};

export const Secondary = {
  args: {
    variant: 'secondary',
    size: 'md',
    disabled: false,
    loading: false,
    children: 'Secondary Button'
  }
};

export const Outline = {
  args: {
    variant: 'outline',
    size: 'md',
    disabled: false,
    loading: false,
    children: 'Outline Button'
  }
};

export const Small = {
  args: {
    variant: 'primary',
    size: 'sm',
    disabled: false,
    loading: false,
    children: 'Small Button'
  }
};

export const Large = {
  args: {
    variant: 'primary',
    size: 'lg',
    disabled: false,
    loading: false,
    children: 'Large Button'
  }
};

export const Disabled = {
  args: {
    variant: 'primary',
    size: 'md',
    disabled: true,
    loading: false,
    children: 'Disabled Button'
  }
};

export const Loading = {
  args: {
    variant: 'primary',
    size: 'md',
    disabled: false,
    loading: true,
    children: 'Loading Button'
  }
};

🎨 Figma Plugin Integration

Figma Plugin for Design Tokens

// figma-plugin.js
figma.showUI(__html__);

figma.ui.onmessage = async (msg) => {
  if (msg.type === 'export-tokens') {
    const tokens = await extractDesignTokens();
    figma.ui.postMessage({
      type: 'design-tokens',
      tokens: tokens
    });
  }
};

async function extractDesignTokens() {
  const colors = await extractColorTokens();
  const typography = await extractTypographyTokens();
  const spacing = await extractSpacingTokens();

  return {
    colors,
    typography,
    spacing
  };
}

async function extractColorTokens() {
  const styles = figma.getLocalPaintStyles();
  return styles
    .filter(style => style.paints.length > 0)
    .map(style => ({
      name: style.name,
      value: style.paints[0].color,
      type: 'color'
    }));
}

async function extractTypographyTokens() {
  const styles = figma.getLocalTextStyles();
  return styles.map(style => ({
    name: style.name,
    font_family: style.fontName.family,
    font_size: style.fontSize,
    font_weight: style.fontName.style,
    line_height: style.lineHeight
  }));
}

async function extractSpacingTokens() {
  // Extract spacing from design tokens
  const styles = figma.getLocalPaintStyles();
  return styles
    .filter(style => style.name.includes('spacing'))
    .map(style => ({
      name: style.name,
      value: style.description,
      type: 'spacing'
    }));
}

Next: Component Library Design Tools