Skip to content

Design System

This guide is for developers who want to use the Easy8 design system in Vue.js and Rails.

Overview

The design system has two main layers:

  • Vue.js components in app/frontend/src/design_system/
  • Rails helpers and custom-element wrappers in app/easy_design/

In practice, Rails renders easy8-ds-* custom elements and the frontend entrypoint registers the matching Vue components.

Related paths:

  • app/frontend/src/design_system/: source of DS Vue components
  • app/frontend/src/shared_modules/: higher-level shared components built on top of the DS
  • app/easy_design/design_system/: Ruby wrappers and helpers for server-rendered usage
  • easy_storybook/: Storybook configuration and development setup

Discover Components

Start with Storybook before writing UI code.

  • Built Storybook in Rails: /easy_storybook
  • Local Storybook: yarn storybook
  • Local DS-only Storybook: yarn storybook-ds - it starts Storybook only with design system styles and without easy styles, for performance optimization (easy_styles.css is too big)

Local Storybook runs on http://localhost:6006.

If you need to rebuild the static Storybook served by Rails, use:

yarn build-storybook

This builds Storybook into public/easy_storybook/. Use local Storybook on port 6006 when you want to inspect current changes without rebuilding the static bundle.

Each DS component has its own lowerCamelCase story file in app/frontend/src/design_system/**/<component>.stories.ts, for example dsRadioGroup.stories.ts.

Use the Design System in Vue.js

Import components from the barrel export when possible:

<template>
  <DSButton variant="primary" size="md" prefix-icon="plus">
    Create project
  </DSButton>

  <DSTextField
    v-model="name"
    name="project_name"
    label="Project name"
    placeholder="Enter project name"
  />
</template>

<script setup lang="ts">
import { ref } from "vue";
import { DSButton, DSTextField } from "@/src/design_system";

const name = ref("");
</script>

Guidelines:

  • Prefer @/src/design_system exports for DS components
  • Check the matching *.stories.ts file before guessing prop names
  • Prefer shared modules from app/frontend/src/shared_modules/ for higher-level use cases
  • For entity autocomplete, prefer the shared Autocomplete module instead of assembling DSSelect manually

Use the Design System in Rails

For server-rendered UI, prefer DS helpers such as ds_button, ds_alert, ds_select, and ds_datepicker.

These helpers render DS custom elements such as easy8-ds-button. The registration happens in app/frontend/entrypoints/easy_ds_custom_elements.ts.

The mapping is intentionally close across layers, for example DSButton in Vue.js, ds_button in Rails, and <easy8-ds-button> as the custom element.

Example:

<%= ds_button(
  variant: :primary,
  size: :md,
  prefix_icon: "plus"
) do %>
  Create project
<% end %>

<%= ds_alert(
  variant: :warning,
  header: "Unsaved changes",
  body: "Your changes are not saved yet.",
  closeable: true
) %>

Guidelines:

  • Prefer ds_* helpers over raw HTML when a DS component already exists
  • Prefer ds_* helpers over writing raw easy8-ds-* tags directly
  • Check the component story for the Rails equivalent in railsCodeTemplate

Use the Design System in Rails Forms

For DS-backed forms, prefer easy_form_for or easy_form_tag. They use EasyFormBuilder and render DS form fields automatically.

Example:

<%= easy_form_for model: @project do |f| %>
  <%= ds_grid do %>
    <%= ds_grid_layout(distribution: :cols_2) do %>
      <%= ds_grid_item do %>
        <%= f.text_field :name, required: true %>
      <% end %>

      <%= ds_grid_item do %>
        <%= f.datepicker :start_date, clearable: true %>
      <% end %>
    <% end %>
  <% end %>

  <%= f.check_box :is_public, label: "Public project" %>
  <%= f.submit "Save" %>
<% end %>

Guidelines:

  • Prefer f.* builder methods inside easy_form_for
  • Use ds_grid, ds_grid_layout, and ds_grid_item for multi-field layouts
  • Do not mix classic Rails form helpers with DS form helpers unless there is a real compatibility reason

CSS Variables

The design system relies on CSS variables and shared tokens.

To check whether all used CSS variables are defined, run:

bundle exec rake easyproject:design_system_css_variables

What it does:

  • scans SCSS files in app/frontend/src/design_system/
  • checks variables used by DS components
  • compares them with variables defined in app/frontend/src/design_system/shared/stylesheets/
  • reports variables that are used but not defined

This is useful after adding or refactoring component styles.

Registry Snapshot and Contract Tests

The Vue components and the Ruby wrappers are two views of the same component API. Contract tests keep them in sync, so a prop added on one side cannot silently drift from the other.

The contract is mediated by a committed snapshot:

  • app/easy_design/design_system/registry_snapshot.json: generated description of every DS custom element (props with types, prop type hints, model value type, exposed methods, and slots)

The snapshot is derived from the Vue components. Never edit it by hand. Regenerate it with:

yarn ds:registry:export

Three test layers check the contract:

  • A vitest freshness guard (app/frontend/src/tests/tests/design_system/tests/dsRegistrySnapshot.spec.ts) fails when the committed snapshot no longer matches the Vue components
  • A vitest registry contract (dsRegistryContract.spec.ts in the same folder) verifies that the custom elements registry (DS_CUSTOM_ELEMENTS_REGISTRY in app/frontend/src/design_system/shared/constants/customElements.ts) describes real components: every Boolean, Number, and Date prop has a PROP_TYPE_HINT (without one, data-* attributes reach Vue as raw strings and "false" is truthy), exposed methods exist on a mounted instance, and the model value type matches
  • An RSpec contract (spec/easy_design/design_system/registry_contract_spec.rb) renders every Ruby DS component with sentinel values derived from the snapshot and checks the emitted data-* attributes and <script data-prop> props against the Vue runtime props. Every Vue prop must be settable through a Ruby component param, and every component param must be accepted by its ds_* helper

Props that are intentionally not exposed on the Ruby side (for example frontend-only validation rules) are exempted in spec/easy_design/design_system/registry_contract_config.yml. Every exemption needs a documented reason.

Typical workflow when changing a DS component API:

  1. Change the Vue component props, slots, or exposed methods, and update the registry entry in shared/constants/customElements.ts
  2. Run yarn ds:registry:export and commit the regenerated snapshot
  3. If the RSpec contract reports a missing param, update the Ruby wrapper in app/easy_design/design_system/components/ and its ds_* helper, or add a documented exemption to registry_contract_config.yml

Rules of Thumb

  • Use a DS component whenever one already exists
  • Check Storybook first
  • Treat story files as the source of truth for component API
  • In Vue, import from @/src/design_system unless you need a more specific path
  • In Rails, prefer ds_* helpers and easy_form_for
  • Prefer shared modules for higher-level functionality
  • Do not hard-code DS colors or spacing values in component styles
  • After changing a component's public API, run yarn ds:registry:export and commit the regenerated snapshot