home / skills / maquina-app / maquina_components / skill

skill skill

/skill

This skill helps you build production-quality Rails UIs with maquina_components, using Tailwind and data attributes to ensure consistent, accessible interfaces.

npx playbooks add skill maquina-app/maquina_components --skill skill

Review the files below or copy the command above to add this skill to your agents.

Files (8)
SKILL.md
13.8 KB
---
name: maquina-ui-standards
description: Build consistent, accessible UIs in Rails using maquina_components. Use this skill when implementing UI for features, creating views, building forms, or reviewing UI specs. Triggers on view creation, UI implementation, form building, layout design, or mentions of maquina_components usage.
---

# Maquina UI Standards

Build production-quality Rails UIs with maquina_components — ERB partials styled with Tailwind CSS 4 and data attributes, inspired by shadcn/ui.

## When to Use This Skill

- Implementing UI for a feature spec
- Creating new views or pages
- Building forms with validation
- Designing page layouts
- Reviewing UI implementation for consistency

## Quick Reference

### Component Rendering

```erb
<%# Partial components %>
<%= render "components/card" do %>
  <%= render "components/card/header" do %>
    <%= render "components/card/title", text: "Title" %>
  <% end %>
<% end %>

<%# Data-attribute components (forms) %>
<%= f.text_field :email, data: { component: "input" } %>
<%= f.submit "Save", data: { component: "button", variant: "primary" } %>
```

### Decision Framework

| Need | Component | Why |
|------|-----------|-----|
| Container with header/content/footer | **Card** | Structured content grouping |
| Important message to user | **Alert** | Draws attention, semantic variants |
| Status indicator | **Badge** | Compact, inline status |
| Data display | **Table** | Structured rows/columns |
| No data state | **Empty** | Consistent empty patterns |
| User actions menu | **Dropdown Menu** | Accessible, keyboard-navigable |
| Selection from options | **Toggle Group** | Visual, single/multi select |
| Selection from many options | **Combobox** | Searchable, filterable |
| Date selection (inline) | **Calendar** | Always-visible date picker |
| Date selection (popover) | **Date Picker** | Compact form date input |
| Page location | **Breadcrumbs** | Navigation context |
| Large result sets | **Pagination** | Pagy integration |
| App navigation | **Sidebar** | Collapsible, persistent state |
| Temporary notifications | **Toast** | Non-intrusive feedback |
| Form inputs | **Form components** | Consistent styling via data attrs |

## File References

| File | Content |
|------|---------|
| [component-catalog.md](component-catalog.md) | All components with props, variants, examples |
| [layout-patterns.md](layout-patterns.md) | Page structure, grids, responsive design |
| [form-patterns.md](form-patterns.md) | Forms, validation, field groups |
| [turbo-integration.md](turbo-integration.md) | Frames, Streams, Morph with components |
| [spec-checklist.md](spec-checklist.md) | UI implementation checklist for specs |

## Core Principles

### 1. Composition Over Configuration

Components are small, composable building blocks. You have full flexibility in how you use them:

**Option A: Use partials as-is** — Render maquina_components partials directly for standard patterns.

**Option B: Compress complexity** — Wrap multiple partials into your own application-specific components that encode your conventions.

**Option C: Copy and adapt** — Copy component partials into your app and customize for specific needs.

```erb
<%# Option A: Direct partial usage %>
<%= render "components/card" do %>
  <%= render "components/card/header" do %>
    <%= render "components/card/title", text: @resource.name %>
  <% end %>
<% end %>

<%# Option B: Application-specific wrapper %>
<%# app/views/components/_resource_card.html.erb %>
<%= render "components/card" do %>
  <%= render "components/card/header", layout: :row do %>
    <div>
      <%= render "components/card/title", text: resource.name %>
      <%= render "components/card/description", text: resource.summary %>
    </div>
    <%= render "components/card/action" do %>
      <%= yield :actions if content_for?(:actions) %>
    <% end %>
  <% end %>
  <%= render "components/card/content" do %>
    <%= yield %>
  <% end %>
<% end %>

<%# Usage: <%= render "components/resource_card", resource: @user do %>...content...<% end %>

<%# Option C: Copied and customized for booking-specific needs %>
<%# app/views/bookings/_booking_card.html.erb - your own component %>
```

Choose the approach that best fits your use case. The goal is consistency within your application, not rigid adherence to a single pattern.

```erb
<%# ✅ GOOD: Compose from parts %>
<%= render "components/card" do %>
  <%= render "components/card/header", layout: :row do %>
    <div>
      <%= render "components/card/title", text: @resource.name %>
      <%= render "components/card/description", text: @resource.summary %>
    </div>
    <%= render "components/card/action" do %>
      <%= link_to "Edit", edit_path, data: { component: "button", variant: "outline", size: "sm" } %>
    <% end %>
  <% end %>
  <%= render "components/card/content" do %>
    <!-- Content -->
  <% end %>
<% end %>

<%# ❌ BAD: Trying to configure everything via props %>
<%= render "components/card", 
    title: @resource.name, 
    description: @resource.summary,
    action_text: "Edit",
    action_path: edit_path %>
```

### 2. Inline Errors Over Error Lists

**Always prefer inline field errors with a flash message** over an alert containing a list of all validation errors.

```erb
<%# ✅ RECOMMENDED: Inline errors + flash %>
<%= form_with model: @user, data: { component: "form" } do |f| %>
  <div data-form-part="group">
    <%= f.label :email, data: { component: "label" } %>
    <%= f.email_field :email, data: { component: "input" } %>
    <% if @user.errors[:email].any? %>
      <p data-form-part="error"><%= @user.errors[:email].first %></p>
    <% end %>
  </div>
  <%# Flash shows brief summary: "Please fix the errors below" %>
<% end %>

<%# ❌ AVOID: Alert with error list %>
<% if @user.errors.any? %>
  <%= render "components/alert", variant: :destructive do %>
    <ul>
      <% @user.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
    </ul>
  <% end %>
<% end %>
```

Inline errors are more accessible — users see the problem next to the field that needs fixing. The flash provides a brief notification that something needs attention.

### 3. Robust Input Attributes

**Every input should have appropriate HTML5 attributes** for validation, accessibility, and mobile optimization:

| Attribute | Purpose | Example |
|-----------|---------|---------|
| `type` | Correct keyboard, validation | `email`, `tel`, `url`, `number` |
| `required` | Mark mandatory fields | `required: true` |
| `maxlength` | Prevent overflow, guide user | `maxlength: 100` |
| `minlength` | Minimum input | `minlength: 2` |
| `pattern` | Custom validation | `pattern: "[0-9]{10}"` |
| `inputmode` | Mobile keyboard hint | `inputmode: "numeric"` |
| `autocomplete` | Autofill hints | `autocomplete: "email"` |

```erb
<%# ✅ GOOD: Complete input attributes %>
<%= f.email_field :email,
    data: { component: "input" },
    required: true,
    maxlength: 254,
    autocomplete: "email",
    placeholder: "[email protected]" %>

<%= f.phone_field :phone,
    data: { component: "input" },
    required: true,
    maxlength: 20,
    pattern: "[+]?[0-9\\s\\-()]+",
    inputmode: "tel",
    autocomplete: "tel" %>

<%= f.text_field :name,
    data: { component: "input" },
    required: true,
    minlength: 2,
    maxlength: 100,
    autocomplete: "name" %>

<%# ❌ BAD: Missing attributes %>
<%= f.text_field :name, data: { component: "input" } %>
```

### 4. Data Attributes for Styling

Components use `data-component` and `data-*-part` attributes. CSS targets these, not classes:

```erb
<%# Component identifies itself %>
<div data-component="card">
  <div data-card-part="header">...</div>
  <div data-card-part="content">...</div>
</div>

<%# Variants via data attributes %>
<%= render "components/badge", variant: :success do %>Active<% end %>
<%# Renders: <span data-component="badge" data-variant="success">Active</span> %>
```

### 5. Form Components via Rails Helpers

Form elements don't need partials — use data attributes directly:

```erb
<%= form_with model: @user, data: { component: "form" } do |f| %>
  <div data-form-part="group">
    <%= f.label :email, data: { component: "label" } %>
    <%= f.email_field :email, data: { component: "input" } %>
  </div>
  
  <div data-form-part="actions">
    <%= f.submit "Save", data: { component: "button", variant: "primary" } %>
  </div>
<% end %>
```

### 6. Icons via Helper

Use `icon_for` helper, which delegates to your app's icon system:

```erb
<%= icon_for :check, class: "size-4" %>
<%= icon_for :chevron_right, class: "size-4 text-muted-foreground" %>
```

### 7. Theme Variables

Colors come from CSS variables (shadcn/ui convention):

| Variable | Usage |
|----------|-------|
| `--primary` / `--primary-foreground` | Primary actions, CTAs |
| `--secondary` / `--secondary-foreground` | Secondary elements |
| `--muted` / `--muted-foreground` | Subdued content |
| `--accent` / `--accent-foreground` | Highlights |
| `--destructive` / `--destructive-foreground` | Dangerous actions |
| `--card` / `--card-foreground` | Card backgrounds |
| `--border` | Borders |
| `--ring` | Focus rings |

## Implementation Workflow

### Step 1: Identify Components Needed

Read the feature spec. Map UI requirements to components:

```
Feature: User Dashboard
- Summary cards → Card (stats variant)
- User list → Table with Badge for status
- Empty state when no users → Empty
- Actions per row → Dropdown Menu
- Page navigation → Pagination
```

### Step 2: Plan Layout Structure

Decide grid/layout before coding:

```erb
<%# Dashboard layout %>
<div class="space-y-6">
  <%# Stats row %>
  <div class="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
    <%= render "dashboard/stat_card", ... %>
  </div>
  
  <%# Main content %>
  <%= render "components/card" do %>
    ...
  <% end %>
</div>
```

### Step 3: Build with Components

Use component catalog, follow composition patterns.

### Step 4: Verify Against Checklist

Run through [spec-checklist.md](spec-checklist.md) before marking complete.

## Anti-Patterns

| ❌ Don't | ✅ Do Instead |
|----------|---------------|
| Inline Tailwind for component styling | Use data attributes, let CSS handle it |
| Create custom card/alert/badge divs | Use maquina_components |
| Skip empty states | Always handle zero-data case |
| Hardcode button styles | Use `data-component="button"` |
| Forget loading/disabled states | Include all interaction states |
| Mix icon libraries | Use `icon_for` consistently |
| Nest components incorrectly | Follow documented composition |
| Skip accessibility attributes | Include ARIA labels, roles |

## Haab-Specific Patterns

For the Haab project, these additional conventions apply:

### Brand Colors

Override theme variables in application.css:

```css
:root {
  --primary: oklch(0.467 0.175 3.95);      /* Dusty Rose #BE185D */
  --primary-foreground: oklch(0.985 0 0);
}
```

### Booking Status Badges

```erb
<% variant = case booking.status
   when "confirmed" then :success
   when "pending" then :warning
   when "cancelled", "no_show" then :destructive
   else :secondary
   end %>
<%= render "components/badge", variant: variant do %>
  <%= t("enums.booking.status.#{booking.status}") %>
<% end %>
```

### Money Display

```erb
<%= render "components/badge", variant: :outline do %>
  <%= format_money(service.price_cents) %>
<% end %>
```

### Time Display

```erb
<%# Use monospace for times %>
<span class="font-mono text-sm">
  <%= l(booking.starts_at, format: :time) %>
</span>
```

## Quick Component Examples

### Card with Action Header

```erb
<%= render "components/card" do %>
  <%= render "components/card/header", layout: :row do %>
    <div>
      <%= render "components/card/title", text: t(".title") %>
      <%= render "components/card/description", text: t(".description") %>
    </div>
    <%= render "components/card/action" do %>
      <%= link_to new_resource_path, data: { component: "button", variant: "primary", size: "sm" } do %>
        <%= icon_for :plus, class: "size-4 mr-1" %><%= t(".add") %>
      <% end %>
    <% end %>
  <% end %>
  <%= render "components/card/content" do %>
    <!-- Content here -->
  <% end %>
<% end %>
```

### Table with Actions

```erb
<%= render "components/table" do %>
  <%= render "components/table/header" do %>
    <%= render "components/table/row" do %>
      <%= render "components/table/head" do %><%= t(".name") %><% end %>
      <%= render "components/table/head" do %><%= t(".status") %><% end %>
      <%= render "components/table/head", css_classes: "w-10" do %>
        <span class="sr-only"><%= t(".actions") %></span>
      <% end %>
    <% end %>
  <% end %>
  <%= render "components/table/body" do %>
    <% @items.each do |item| %>
      <%= render "components/table/row" do %>
        <%= render "components/table/cell" do %><%= item.name %><% end %>
        <%= render "components/table/cell" do %>
          <%= render "components/badge", variant: status_variant(item) do %>
            <%= item.status.titleize %>
          <% end %>
        <% end %>
        <%= render "components/table/cell" do %>
          <%= render "shared/row_actions", item: item %>
        <% end %>
      <% end %>
    <% end %>
  <% end %>
<% end %>
```

### Empty State

```erb
<% if @items.empty? %>
  <%= render "components/empty" do %>
    <%= render "components/empty/header" do %>
      <%= render "components/empty/media" do %>
        <div class="flex h-16 w-16 items-center justify-center rounded-full bg-muted">
          <%= icon_for :inbox, class: "size-8 text-muted-foreground" %>
        </div>
      <% end %>
      <%= render "components/empty/title", text: t(".empty.title") %>
      <%= render "components/empty/description", text: t(".empty.description") %>
    <% end %>
    <%= render "components/empty/content" do %>
      <%= link_to t(".empty.action"), new_path, data: { component: "button", variant: "primary" } %>
    <% end %>
  <% end %>
<% end %>
```

See individual reference files for complete documentation.

Overview

This skill helps you build consistent, accessible Rails UIs using maquina_components — ERB partials styled with Tailwind CSS and Stimulus. It codifies composition patterns, form conventions, and design tokens so teams ship predictable views and interactions. Use it to implement pages, forms, lists, and interactive components with accessibility and theme variables in mind.

How this skill works

The skill inspects feature specs, view creation, form implementations, and mentions of maquina_components usage to recommend components and patterns. It maps UI needs to the component catalog (cards, badges, tables, empty states, dropdowns, etc.), enforces data-attribute styling, and guides input attributes and error handling. It also provides layout and verification workflows to validate implementation against a spec checklist.

When to use it

  • Implementing UI for a feature or spec
  • Creating new views, pages, or layouts
  • Building forms with validation and accessible inputs
  • Reviewing UI implementation for consistency and accessibility
  • Adding or refactoring component composition and theming

Best practices

  • Compose small partials instead of configuring monolithic props—favor composition over configuration
  • Use data attributes (data-component, data-*-part) for styling; avoid inline Tailwind in components
  • Prefer inline field errors with a brief flash summary instead of a single alert listing all errors
  • Add robust HTML5 attributes (type, required, maxlength, inputmode, autocomplete, pattern) on inputs
  • Use icon_for helper and a single icon system; include ARIA attributes and focus states
  • Handle empty states, loading, and disabled states explicitly; follow the spec checklist before marking complete

Example use cases

  • Dashboard: grid of stat cards, main table with badges and row actions, pagination integration
  • Form screen: form_with + data-component=form, labeled inputs with required/maxlength, inline errors and submit button variant
  • Index page: render components/table with header/body partials and shared row_actions for per-row menus
  • Empty state: components/empty with media, title, description, and primary action button
  • Booking UI (Haab): status badges mapped to variants, themed color variables, money and time display patterns

FAQ

Should I copy components into my app or render them directly?

Either is fine. Render partials directly for standard patterns, wrap them in app-specific wrappers for conventions, or copy and adapt when you need deep customization. Choose the approach that preserves consistency for your app.

How should I handle validation errors?

Prefer inline errors next to each field and a short flash summary. This improves accessibility and helps users fix the exact field that failed validation.