Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/assets/stylesheets/servactory/web/compiled.css

Large diffs are not rendered by default.

240 changes: 209 additions & 31 deletions app/components/servactory/web/ui_kit/README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
# Servactory Web UI Kit

The UI Kit is a set of reusable ViewComponent-based components for the Servactory Web interface. All components are built according to atomic design principles (atoms, molecules, organisms), are easy to extend, and cover the main UI needs of the project.
The UI Kit is a set of reusable [ViewComponent](https://viewcomponent.org/) components for the Servactory Web interface. All components are built according to atomic design principles (atoms, molecules, organisms), are easy to extend, and cover the main UI needs of the project.

## Atomic Design
## Atomic Design & Folder Structure

- **Atoms** — basic elements (buttons, icons, badges, links).
- **Molecules** — simple compositions of atoms (section headers, list items, containers).
- **Organisms** — complex blocks combining molecules and atoms (cards, lists, service tree, navigation).
- **Atoms** — basic elements (buttons, icons, badges, links). Located in `app/components/servactory/web/ui_kit/atoms/`.
- **Molecules** — simple compositions of atoms (section headers, list items, containers). Located in `app/components/servactory/web/ui_kit/molecules/`.
- **Organisms** — complex blocks combining molecules and atoms (cards, lists, service tree, navigation). Located in `app/components/servactory/web/ui_kit/organisms/`.

## General Rules
All components inherit from `ViewComponent::Base`.

- Each component consists of a Ruby class and a template (`.rb` + `.html.erb`).
- All components are located in `app/components/servactory/web/ui_kit` according to atomic design.
- **All components support the `class_name:` parameter for customizing with TailwindCSS utility classes.**
- **To pass standard HTML attributes, use the `options:` parameter (e.g., aria-label, data-*, target, etc.).**
- **Use only TailwindCSS 4.1 utility classes. Do not add custom CSS classes.**
- Do not use `options` unless you need to pass standard HTML attributes.
**All components support the `class_name:` parameter for customizing with TailwindCSS utility classes.**
**To pass standard HTML attributes, use the `options:` parameter (e.g., aria-label, data-*, target, etc.).**
**Use only TailwindCSS 4.1 utility classes. Do not add custom CSS classes.**

---

Expand All @@ -27,7 +24,7 @@ SVG icon (file, folder, etc.).
<%= render IconComponent.new(name: :file, class_name: 'w-6 h-6 text-blue-500') %>
```
**Parameters:**
- `name:` — icon name (`:file`, `:folder`, `:inputs`, `:internals`, `:outputs`, `:actions`, `:code`, `:custom`)
- `name:` — icon name (`:file`, `:folder`, `:inputs`, `:internals`, `:outputs`, `:actions`, `:code`, `:copy`, `:stack`)
- `class_name:` — TailwindCSS utility classes for the SVG element (required: always specify size and color)

### LinkComponent
Expand All @@ -50,26 +47,49 @@ Badge with text, can be used for required/optional indicators.
- `text:` — badge text
- `class_name:` — TailwindCSS utility classes

### ColoredSectionHeaderComponent
Colored section header with a left border.
```erb
<%= render ColoredSectionHeaderComponent.new(
title: 'Inputs',
border_class: 'border-blue-500',
bg_class: 'bg-blue-100/60',
text_class: 'text-blue-700',
class_name: 'mb-2',
options: { id: 'inputs-header' }
) %>
```
**Parameters:**
- `title:` — header text
- `border_class:` — utility class for left border
- `bg_class:` — utility class for background
- `text_class:` — utility class for text
- `class_name:` — additional utility classes
- `options:` — standard HTML attributes

### CopyButtonComponent
Button for copying code.
```erb
<%= render CopyButtonComponent.new(code: 'def foo; end') %>
<%= render CopyButtonComponent.new(code: 'def foo; end', options: { id: 'copy-btn' }) %>
```
**Parameters:**
- `code:` — code to copy
- `options:` — standard HTML attributes

### CardHeaderTextComponent
Заголовок для карточек и секций.
Header for cards and sections.
```erb
<%= render CardHeaderTextComponent.new(text: 'My Title', class_name: 'mb-2') %>
<%= render CardHeaderTextComponent.new(text: 'My Title', class_name: 'mb-2', options: { id: 'header' }) %>
```
**Parameters:**
- `text:` — заголовок
- `text:` — header
- `class_name:` — TailwindCSS utility classes
- `options:` — стандартные HTML-атрибуты
- `options:` — standard HTML attributes

### EmptyStateComponent
Empty state for lists.
```erb
<%= render EmptyStateComponent.new(message: 'No data') %>
<%= render EmptyStateComponent.new(message: 'No data', class_name: 'mt-4', options: { id: 'empty' }) %>
```
**Parameters:**
- `message:` — message to display
Expand All @@ -83,18 +103,42 @@ Empty state for lists.
### SectionHeaderComponent
Section header with icon.
```erb
<%= render SectionHeaderComponent.new(title: 'Inputs', icon_name: :file, class_name: 'text-blue-600') %>
<%= render SectionHeaderComponent.new(title: 'Inputs', icon_name: :file, class_name: 'text-blue-600', options: { id: 'inputs-section' }) %>
```
**Parameters:**
- `title:` — header text
- `icon_name:` — icon name (see IconComponent)
- `class_name:` — utility classes
- `options:` — HTML attributes

### AttributeSectionComponent
Section with colored header and attribute list.
```erb
<%= render AttributeSectionComponent.new(
title: 'Inputs',
items: @inputs,
border_class: 'border-blue-500',
text_class: 'text-blue-700',
bg_class: 'bg-blue-100/60',
empty_message: 'No input attributes',
class_name: 'mb-4',
options: { id: 'inputs-section' }
) %>
```
**Parameters:**
- `title:` — section title
- `items:` — attributes to display
- `border_class:` — utility class for left border
- `bg_class:` — utility class for background
- `text_class:` — utility class for text
- `empty_message:` — message to display when no items
- `class_name:` — additional utility classes
- `options:` — standard HTML attributes

### AttributeItemComponent
Attribute list item (name, required/optional, description).
```erb
<%= render AttributeItemComponent.new(name: 'user_id', border_class: 'border-blue-500', text_class: 'text-blue-700', bg_class: 'bg-blue-50', attribute: attr_obj) do %>
<%= render AttributeItemComponent.new(name: 'user_id', border_class: 'border-blue-500', text_class: 'text-blue-700', bg_class: 'bg-blue-50', attribute: attr_obj, class_name: 'mb-2', options: { id: 'user-id-attr' }) do %>
...
<% end %>
```
Expand All @@ -106,20 +150,62 @@ Attribute list item (name, required/optional, description).
- `attribute:` — attribute object (determines required)
- `class_name:`, `options:`

### CardBodyComponent / CardBodyContainerComponent / CardContainerComponent
Internal containers for cards, support customization via class_name and options.
### CardBodyComponent
Container for card body content.
```erb
<%= render CardBodyComponent.new(class_name: 'p-4', options: { id: 'card-body' }) do %>
...
<% end %>
```
**Parameters:**
- `class_name:` — utility classes
- `options:` — standard HTML attributes

### CardBodyContainerComponent
Internal container for card body.
```erb
<%= render CardBodyContainerComponent.new(class_name: 'p-4', options: { id: 'body-container' }) do %>
...
<% end %>
```
**Parameters:**
- `class_name:` — utility classes
- `options:` — standard HTML attributes

### CardContainerComponent
Container for card content.
```erb
<%= render CardContainerComponent.new(class_name: 'shadow', options: { id: 'container' }) do %>
...
<% end %>
```
**Parameters:**
- `class_name:` — utility classes
- `options:` — standard HTML attributes

### CardHeaderComponent
Card header, supports customization.
Card header, supports a `right` slot for actions.
```erb
<%= render CardHeaderComponent.new(text: 'Header', class_name: 'bg-gray-50', options: { id: 'header' }) do |header| %>
<% header.right do %>
...actions...
<% end %>
<% end %>
```
**Parameters:**
- `text:` — header text
- `class_name:` — utility classes
- `options:` — standard HTML attributes
- `right` slot for actions

---

## Organisms

### CardComponent
Universal container for sections/content.
Universal container for sections/content. Supports a `header` slot.
```erb
<%= render CardComponent.new(class_name: 'shadow-lg') do |card| %>
<%= render CardComponent.new(class_name: 'shadow-lg', options: { id: 'main-card' }) do |card| %>
<% card.with_header do %>
...
<% end %>
Expand All @@ -131,18 +217,24 @@ Universal container for sections/content.
- `header` slot for the header

### SectionCardComponent
Section card with header, icon, and attribute list (inputs, outputs, actions, etc.). Composes CardComponent, SectionHeaderComponent, AttributeListComponent.
Section card with header, icon, and attribute list (inputs, outputs, actions, etc.).
```erb
<%= render SectionCardComponent.new(title: 'Inputs', items: {...}, border_class: 'border-blue-500', text_class: 'text-blue-700', bg_class: 'bg-blue-50', icon_name: :inputs, empty_message: 'No input attributes') %>
<%= render SectionCardComponent.new(title: 'Inputs', items: @items, border_class: 'border-blue-500', text_class: 'text-blue-700', bg_class: 'bg-blue-50', icon_name: :inputs, empty_message: 'No input attributes', class_name: 'mb-4', options: { id: 'inputs-section-card' }) %>
```
**Parameters:**
- `title:`, `items:`, `border_class:`, `text_class:`, `bg_class:`, `icon_name:`, `empty_message:`, `class_name:`, `options:`
- `body` slot for additional content

### AttributeListComponent
List of attributes (inputs, outputs, internals, actions).
```erb
<%= render AttributeListComponent.new(items: {...}, border_class: 'border-blue-500', text_class: 'text-blue-700', bg_class: 'bg-blue-50', empty_message: 'No attributes') %>
<%= render AttributeListComponent.new(items: @items, border_class: 'border-blue-500', text_class: 'text-blue-700', bg_class: 'bg-blue-50', empty_message: 'No attributes', class_name: 'mb-2', options: { id: 'attr-list' }) %>
```
**Parameters:**
- `items:` — attributes hash
- `border_class:`, `text_class:`, `bg_class:` — utility classes
- `empty_message:` — message if no items
- `class_name:`, `options:`

### CodeBlockComponent
Block with source code and copy button.
Expand All @@ -152,29 +244,115 @@ Block with source code and copy button.
**Parameters:**
- `code:`, `language:`, `copy_button:`

### TreeComponent / TreeNodeComponent
### AttributesBlockComponent
Block containing multiple attribute sections (inputs, internals, outputs).
```erb
<%= render AttributesBlockComponent.new(
inputs: @inputs,
internals: @internals,
outputs: @outputs,
class_name: 'mb-4',
options: { id: 'attr-block' }
) %>
```
**Parameters:**
- `inputs:` — input attributes
- `internals:` — internal attributes
- `outputs:` — output attributes
- `class_name:` — additional utility classes
- `options:` — standard HTML attributes
- Conditional rendering: only renders if at least one attribute group is present

### ServiceDetailsComponent
Complete service details page with header, attributes, actions, and code.
```erb
<%= render ServiceDetailsComponent.new(
service_class: @service_class,
source_code: @source_code,
class_name: 'mb-4',
options: { id: 'service-details' }
) %>
```
**Parameters:**
- `service_class:` — service class object
- `source_code:` — service source code
- `class_name:` — additional utility classes
- `options:` — standard HTML attributes
- Conditional rendering: only renders if both service_class and source_code are present

### ServiceNotFoundComponent
Service not found page.
```erb
<%= render ServiceNotFoundComponent.new(class_name: 'mb-4', options: { id: 'not-found' }) %>
```
**Parameters:**
- `class_name:` — additional utility classes
- `options:` — standard HTML attributes

### TreeComponent
Service tree (service navigation).
```erb
<%= render TreeComponent.new(nodes: @services_tree) %>
<%= render TreeComponent.new(nodes: @services_tree, route_type: :internal, class_name: 'mb-4', options: { id: 'tree' }) %>
```
**Parameters:**
- `nodes:` — array of tree nodes
- `route_type:` — :internal or :external
- `gem_name:` — (optional) gem name for external tree
- `class_name:`, `options:`

### TreeNodeComponent
Single node in the service tree. Used internally by TreeComponent.
**Parameters:**
- `node:` — node hash
- `route_type:` — :internal or :external
- `level:` — (optional) tree depth
- `gem_name:` — (optional) gem name

### NavbarComponent
Top navigation bar.
```erb
<%= render NavbarComponent.new(app_name: 'MyApp', app_url: '/', documentation_url: '/docs', github_url: 'https://github.com/...') %>
```
**Parameters:**
- `app_name:` — application name
- `app_url:` — (optional) application URL
- `documentation_url:` — docs URL
- `github_url:` — GitHub URL

### FooterComponent
Site footer.
```erb
<%= render FooterComponent.new(year: 2024, documentation_url: '/docs', github_url: 'https://github.com/...', version: '1.0.0', release_url: '/releases/1.0.0') %>
```
**Parameters:**
- `year:` — copyright year
- `documentation_url:` — docs URL
- `github_url:` — GitHub URL
- `version:` — version string
- `release_url:` — release URL

### PageHeaderComponent
Page header with description.
```erb
<%= render PageHeaderComponent.new(title: 'Title', description: 'Desc') %>
```
**Parameters:**
- `title:` — page title
- `description:` — (optional) description

### SubnavigationComponent
Navigation bar for switching between Application and Gem services.
```erb
<%= render SubnavigationComponent.new(gem_names: ["my_gem", "other_gem"], active_gem_name: params[:gem_name], class_name: 'mb-4', options: { id: 'subnav' }) %>
```
**Parameters:**
- `gem_names:` — array of gem names
- `active_gem_name:` — (optional) the gem name from params to highlight the active link
- `class_name:` — TailwindCSS utility classes
- `options:` — standard HTML attributes
- Conditional rendering: only renders if gem_names are present

The active link is highlighted with `text-blue-600 font-semibold`.

---

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div class="border-l-4 <%= @border_class %> <%= @bg_class %> <%= @text_class %> py-1 px-3 mb-3 font-semibold text-base <%= @class_name %> <%= @options[:class] %>" <%= tag.attributes(@options.except(:class)) %>>
<%= @title %>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

module Servactory
module Web
module UiKit
module Atoms
class ColoredSectionHeaderComponent < ViewComponent::Base
include Servactory::Web::UiKit::Concerns::ComponentOptions

def initialize(title:, border_class:, bg_class:, text_class:, class_name: nil, options: {})
super()
@title = title
@border_class = border_class
@bg_class = bg_class
@text_class = text_class
initialize_component_options(class_name:, options:)
end
end
end
end
end
end
6 changes: 2 additions & 4 deletions app/components/servactory/web/ui_kit/atoms/icon_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,10 @@ class IconComponent < ViewComponent::Base
ICONS = {
file: '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="%<class_name>s"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" /></svg>',
folder: '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="%<class_name>s"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12.75V12A2.25 2.25 0 0 1 4.5 9.75h15A2.25 2.25 0 0 1 21.75 12v.75m-8.69-6.44-2.12-2.12a1.5 1.5 0 0 0-1.061-.44H4.5A2.25 2.25 0 0 0 2.25 6v12a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9a2.25 2.25 0 0 0-2.25-2.25h-5.379a1.5 1.5 0 0 1-1.06-.44Z" /></svg>',
inputs: '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="%<class_name>s"><path stroke-linecap="round" stroke-linejoin="round" d="M8.25 9V5.25A2.25 2.25 0 0 1 10.5 3h6a2.25 2.25 0 0 1 2.25 2.25v13.5A2.25 2.25 0 0 1 16.5 21h-6a2.25 2.25 0 0 1-2.25-2.25V15M12 9l3 3m0 0-3 3m3-3H2.25" /></svg>',
internals: '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="%<class_name>s"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12c0-1.232-.046-2.453-.138-3.662a4.006 4.006 0 0 0-3.7-3.7 48.678 48.678 0 0 0-7.324 0 4.006 4.006 0 0 0-3.7 3.7c-.017.22-.032.441-.046.662M19.5 12l3-3m-3 3-3-3m-12 3c0 1.232.046 2.453.138 3.662a4.006 4.006 0 0 0 3.7 3.7 48.656 48.656 0 0 0 7.324 0 4.006 4.006 0 0 0 3.7-3.7c.017-.22.032-.441.046-.662M4.5 12l3 3m-3-3-3 3" /></svg>',
outputs: '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="%<class_name>s"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0 0 13.5 3h-6a2.25 2.25 0 0 0-2.25 2.25v13.5A2.25 2.25 0 0 0 7.5 21h6a2.25 2.25 0 0 0 2.25-2.25V15m3 0 3-3m0 0-3-3m3 3H9" /></svg>',
actions: '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="%<class_name>s"><path stroke-linecap="round" stroke-linejoin="round" d="M14.25 9.75 16.5 12l-2.25 2.25m-4.5 0L7.5 12l2.25-2.25M6 20.25h12A2.25 2.25 0 0 0 20.25 18V6A2.25 2.25 0 0 0 18 3.75H6A2.25 2.25 0 0 0 3.75 6v12A2.25 2.25 0 0 0 6 20.25Z" /></svg>',
code: '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="%<class_name>s"><path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75 22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3-4.5 16.5" /></svg>',
copy: '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" class="%<class_name>s"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>'
copy: '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" class="%<class_name>s"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>',
stack: '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="%<class_name>s"><path stroke-linecap="round" stroke-linejoin="round" d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125" /></svg>'
}.freeze
# rubocop:enable Layout/LineLength

Expand Down
Loading