Skip to content
Purple-MagicPublic

About

Start your Rails project with Tramway

Resources

Stars

31 stars

Watchers

3 watching

Forks

Repository files navigation

Tramway

Unite Ruby on Rails brilliance. Streamline development with Tramway.

AI agents: read this first. Before writing or changing any code in a Rails app that uses Tramway, install and use the tramway-skill skill — it is mandatory for Tramway work and is the authoritative, up-to-date playbook for Tramway's conventions (CRUD, Forms, Decorators, Navbar, Plugins, and more). Do not generate Tramway-related code from memory alone.

Tramway ships with full CRUD (index, show, create, update, destroy) out of the box. Its install generator also adds a Codex instruction that points agents to the Tramway skill for Tramway-native code generation.

RubyGems Version Tests Lint RubyGems Downloads (total)

Compatibility

Tramway is actively verified against the following Ruby and Rails versions.

Ruby \ Rails 8.0 8.1
3.2 ✅ ✅
3.3 ✅ ✅
3.4 ✅ ✅
4.0 ✅ ✅

Rails 7 is no longer supported. See the announcement for details.

Installation

Add this line to your application's Gemfile:

gem "tramway"

Then install Tramway and its dependencies:

bundle install
bin/rails g tramway:install

The install generator adds the required gems (haml-rails, kaminari, view_component, dry-initializer, and dry-monads) to your application's Gemfile—if they are not present—appends the Tailwind safelist configuration Tramway ships with, and injects the Trix stylesheet and JavaScript tags into your application layout (required for rich_text_area).

Getting Started

Step 1

config/initializers/tramway.rb

Tramway.configure do |config|
  config.entities = [
    {
      name: :user,
      pages: [
        { action: :index },
        { action: :show },
        { action: :create },
        { action: :update },
        { action: :destroy }
      ],
    }
  ]
end

Step 2

config/routes.rb

Rails.application.routes.draw do
  mount Tramway::Engine, at: '/'
end

Step 3

class UserDecorator < Tramway::BaseDecorator
  delegate_attributes :email, :created_at

  def self.index_attributes
    [:id, :email, :created_at]
  end
end

Step 4

If you ran bin/rails g tramway:install, the install generator already copied the Tailwind safelist to config/tailwind.config.js. Otherwise, copy this file from the gem: config/tailwind.config.js.

Step 5

Run tailwincss-rails compiler

bin/rails tailwindcss:build

Step 6

Run your server

bundle exec rails s

Step 7

Open http://localhost:3000/users

Usage

Tramway CRUD

Tramway is an entity-based framework. Entity is the class on whose objects actions be applied: index, show, create, update, and destroy. Tramway will support numerous classes as entities. For now, Entity could be only ActiveRecord::Base class.

Define entity for Tramway

config/initializers/tramway.rb

Tramway.configure do |config|
  config.entities = [ :user, :podcast, :episode ] # entities based on models User, Podcast and Episode are defined
end

By default, links to the Tramway CRUD index page are rendered in Tramway Navbar.

Define entities with options

Tramway Entity supports several options that are used in different features.

route

config/initializers/tramway.rb

Tramway.configure do |config|
  config.entities = [
    {
      name: :user,
      namespace: :admin
    }, # `/admin/users` link in the Tramway Navbar
    {
      name: :episodes,
      route: {
        route_method: :episodes
      }
    }, # `/podcasts/episodes` link in the Tramway Navbar
  ]
end

scope

By default, Tramway lists all records for an entity on the index page. You can narrow the records displayed by providing a scope. When set, Tramway will call the named scope on the entity before rendering the index view.

config/initializers/tramway.rb

Tramway.configure do |config|
  config.entities = [
    {
      name: :campaign,
      namespace: :admin,
      pages: [
        {
          action: :index,
          scope: :active
        }
      ]
    }
  ]
end

In this example, the Campaign entity will display only records returned by the active scope on its index page, while all other pages continue to show every record unless another scope is specified.

search

Search is disabled by default on index pages. Enable it per-entity by setting search: true on the :index page entry:

config/initializers/tramway.rb

Tramway.configure do |config|
  config.entities = [
    {
      name: :campaign,
      pages: [
        {
          action: :index,
          search: true
        }
      ]
    }
  ]
end

When search is enabled, a text input and a search icon button appear at the top of the index page. Tramway uses Model.search(query) if you define it (e.g. a pg_search_scope or any custom scope named search). If not, it falls back to Model.tramway_search(query), which searches across all string/text columns of the model using the pg_search gem, and logs a warning. The fallback is generic and not tailored to your data structure, so it is not intended for long-term use and may be slow or not scalable.

pg_search requires PostgreSQL's full-text search functions. If your application's database is not PostgreSQL, calling the fallback raises Tramway::Errors::UnsupportedDatabaseAdapterError with an explanation and a suggestion to define your own Model.search(query) scope instead.

bin/rails g tramway:install adds gem "pg_search" to your Gemfile automatically (like Tramway's other dependencies), so run it (and bundle install) if you see the fallback raise Tramway::Errors::MissingGemError. That error also fires if pg_search can't otherwise be loaded, and explains how to fix it or define your own Model.search(query) scope.

includes

If a decorator's index_attributes reads an association (e.g. object.comments.map(&:text)), each row triggers its own query unless the association is eager-loaded. Set includes on the :index page entry to preload it, using the same shape Rails' .includes accepts (a symbol, array, or nested hash):

config/initializers/tramway.rb

Tramway.configure do |config|
  config.entities = [
    {
      name: :campaign,
      namespace: :admin,
      pages: [
        {
          action: :index,
          includes: [:user, :comments]
        }
      ]
    }
  ]
end

In this example, Tramway calls Campaign.includes(:user, :comments) before applying scope/search, so accessing campaign.user or campaign.comments in the decorator's index_attributes does not issue an extra query per row. Omitting includes produces identical behavior to before this option existed.

filters

Filters are disabled by default on index pages. Enable them by listing the scope names you want selectable under filters on the :index page entry:

config/initializers/tramway.rb

Tramway.configure do |config|
  config.entities = [
    {
      name: :campaign,
      namespace: :admin,
      pages: [
        {
          action: :index,
          filters: [:active, :archived]
        }
      ]
    }
  ]
end

When filters are configured, Tramway adds a "Filters" toggle to the same row as the search box and pagination, above the index listing. Clicking it reveals a list of small buttons, one per configured filter, each showing how many records currently match it (e.g. "ACTIVE · 34"). Clicking a button navigates to the index page with that filter applied, keeping any existing search query. The currently applied filter is highlighted, and the toggle remembers whether the list was last left open or closed.

Visiting the index page with ?filter=active calls Campaign.active (the scope must already exist on the model) in addition to any configured scope. If filter does not match one of the configured filters, Tramway raises Tramway::Errors::InvalidFilterError explaining which filters are available. Omitting filters (the default) makes Tramway ignore params[:filter] entirely.

show page

To render a show page for an entity, declare a :show action inside the pages array in config/initializers/tramway.rb. Tramway will generate the route and render a table using the attributes returned by the decorator's 'show_attributes` method.

Tramway.configure do |config|
  config.entities = [
    {
      name: :campaign,
      pages: [
        { action: :index },
        { action: :show }
      ]
    }
  ]
end
class CampaignDecorator < Tramway::BaseDecorator
  def show_attributes
    %i[name status starts_at]
  end
end

With this configuration in place, visiting the show page displays a two-column table where the left column contains the localized attribute names and the right column renders their values.

Each entry in show_attributes can also be a Hash instead of a plain symbol, to pass options to the tramway_cell rendering that attribute's value. The Hash requires an attribute: key (the method to display) and accepts an options: key (a Hash forwarded as-is to tramway_cell, e.g. truncate: false):

class CampaignDecorator < Tramway::BaseDecorator
  def show_attributes
    [:name, { attribute: :status, options: { truncate: false } }, :starts_at]
  end
end

create page

To render a create page for an entity, declare a :create action inside the pages array in config/initializers/tramway.rb. Tramway will generate the route and render the form fields based on your form object. See the form_fields method for details on configuring fields.

Tramway.configure do |config|
  config.entities = [
    {
      name: :campaign,
      pages: [
        { action: :index },
        { action: :create }
      ]
    }
  ]
end

update page

To render an update page, declare an :update action inside the pages array in config/initializers/tramway.rb. Tramway will generate the edit route and reuse the same form partial as create. See the form_fields method for details on configuring fields.

Tramway.configure do |config|
  config.entities = [
    {
      name: :campaign,
      pages: [
        { action: :index },
        { action: :update }
      ]
    }
  ]
end

fields method

Use fields in your form class to customize which form helpers get rendered and which options are passed to them. Each field must map to a form helper method name. When you need to pass options, use a hash where :type is the helper method name and the remaining keys are passed as named arguments.

class UserForm < Tramway::BaseForm
  properties :email, :about_me, :user_type, :score

  fields email: :email,
    name: :text,
    about_me: {
      type: :text_area,
      rows: 5
    },
    user_type: {
      type: :select,
      collection: ['regular', 'user']
    },
    score: {
      type: :number,
      value: -> (object) { Score.find_by(user_id: object.id).value }
    }

  def score=(value)
    Score.find_by(user_id: object.id).update(value:)
  end
end

The configuration above renders:

<%= tramway_form_for .... do |f| %>
  <%= f.email_field :email %>
  <%= f.text_field :name %>
  <%= f.text_area :about_me, rows: 5 %>

destroy page

To render a destroy action, declare a :destroy action inside the pages array in config/initializers/tramway.rb. Tramway will generate the destroy route and render the delete action button.

Tramway.configure do |config|
  config.entities = [
    {
      name: :campaign,
      pages: [
        { action: :index },
        { action: :destroy }
      ]
    }
  ]
end

custom page

Besides the built-in index, show, create, update, and destroy pages, an entity can declare Custom Pages: your own controller action and view, loaded through the same routing, layout, and navbar as the rest of the entity's pages. Use a Custom Page for anything that doesn't fit the built-in CRUD actions — a report, a bulk tool, an "impersonate" button's target page, and so on.

Declare it by adding a page whose action is not one of the built-in ones:

Tramway.configure do |config|
  config.entities = [
    {
      name: :user,
      namespace: :admin,
      pages: [
        { action: :index },
        {
          action: :impersonate,
          member: true,       # nests the route under /:id (default: false, an entity-level route)
          via: :post,         # HTTP method(s) the route accepts (default: :get); also accepts an array, e.g. %i[get post]
          params: [:redirect_to] # extra required path segments, e.g. /admin/users/:id/impersonate/:redirect_to
        }
      ]
    }
  ]
end

Then implement the action yourself, by Rails naming convention, in the controller Tramway would otherwise generate for that namespace/entity (here, Admin::UsersController), inheriting from Tramway::EntitiesController so it gets the same layout, navbar, and entity/model_class lookups as the built-in pages:

# app/controllers/admin/users_controller.rb
module Admin
  class UsersController < Tramway::EntitiesController
    def impersonate
      @user = model_class.find(params[:id])
      # ...
    end
  end
end

And add its view at the conventional Rails path for that controller/action:

-# app/views/admin/users/impersonate.html.haml
= tramway_container do
  %p Impersonating #{@user.email}

See docs/users/tramway_custom_pages.md for how the routing/controller/view resolution works under the hood.

route_helper

To get routes Tramway generated just Tramway::Engine.

Tramway::Engine.routes.url_helpers.users_path => '/admin/users'
Tramway::Engine.routes.url_helpers.podcasts_episodes_path => '/podcasts/episodes'

Tramway Decorators

Tramway provides convenient decorators for your objects. NOTE: This is not the decorator pattern in its usual representation.

app/controllers/users_controller.rb

def index
  # this line of code decorates the users collection with the default UserDecorator
  @users = tramway_decorate User.all 
end

app/decorators/user_decorator.rb

class UserDecorator < Tramway::BaseDecorator
  # delegates attributes to decorated object
  delegate_attributes :email, :first_name, :last_name

  association :posts

  # you can provide your own methods with access to decorated object attributes with the method `object`
  def created_at
    I18n.l object.created_at
  end

  # you can provide representations with ViewComponent to avoid implementing views with Rails Helpers
  def posts_table
    component 'table', object.posts
  end
end

Add header content to index pages

You can inject custom content above an entity's index table by defining an index_header_content lambda on its decorator. The lambda receives the collection of decorated records and can render any component you need. See docs/users/tramway_entities.md for where exactly this content is placed on the page.

config/initializers/tramway.rb

Tramway.configure do |config|
  config.entities = [
    {
      name: :project,
      pages: [
        { action: :index }
      ]
    }
  ]
end

app/decorators/project_decorator.rb

class ProjectDecorator < Tramway::BaseDecorator
  class << self
    def index_header_content
      lambda do |_collection|
        component "projects/index_header"
      end
    end
  end
end

app/components/projects/index_header_component.html.haml

.mb-2
  = tramway_button path: Rails.application.routes.url_helpers.new_project_path,
    text: 'Create',
    type: :hope

app/components/projects/index_header_component.rb

class Projects::IndexHeaderComponent < Tramway::BaseComponent
end

With this configuration in place, the index page will render the Create button and the header content above the table of projects.

Add header content to show pages

To inject custom content above a record's details, define an object-level show_header_content method on its decorator. The method can return any rendered content and has full access to the decorated object's helpers and attributes.

Use view_context in show and index pages

Some helpers—such as button_to with non-GET methods—need access to the live request context to include CSRF tokens. Pass the Rails view_context into your decorated objects so their render calls execute inside the current request.

def show
  @project = tramway_decorate(Project.find(params[:id])).with(view_context:)
end

For index pages, decorate each record with the current context before rendering headers or per-row components:

def index
  @projects = tramway_decorate(Project.all).map { |project| project.with(view_context:) }
end

Passing the context this way ensures show_header_content and index_header_content blocks can safely call helpers that require the session-bound authenticity token.

Decorate a single object

You can use the same method to decorate a single object either

def show
  @user = tramway_decorate User.find params[:id]
end

All objects returned from tramway_decorate respond to with(view_context:), so you can attach the current Rails view_context when you need decorator-rendered content to use helpers that rely on the active request (such as CSRF tokens).

Decorate a collection of objects

def index
  @users = tramway_decorate User.all
end
def index
  @posts = tramway_decorate user.posts
end

Decorate with a specific decorator

You can implement a specific decorator and ask Tramway to decorate with it

def show
  @user = tramway_decorate User.find(params[:id]), decorator: Users::ShowDecorator
end

Decorate associations

Decorate single association

class UserDecorator < Tramway::BaseDecorator
  association :posts
end

user = tramway_decorate User.first
user.posts # => decorated collection of posts with PostDecorator

Decorate multiple associations

class UserDecorator < Tramway::BaseDecorator
  associations :posts, :users
end

Show associations

Entity show pages can render associated records by returning association names from the decorator's show_associations method. Each name must be an actual Active Record association on the decorated model; Tramway raises a clear error when the association is missing.

app/models/post.rb

class Post < ApplicationRecord
  has_many :comments
end

app/decorators/post_decorator.rb

class PostDecorator < Tramway::BaseDecorator
  association :comments

  def show_associations
    %i[comments]
  end
end

Tramway renders a New button for a shown association when it can determine a create path for that association. Define the associated model as a Tramway entity with a :create page to let Tramway build that path automatically:

config/initializers/tramway.rb

Tramway.configure do |config|
  config.entities = [
    {
      name: :post,
      pages: [
        { action: :show }
      ]
    },
    {
      name: :comment,
      pages: [
        { action: :create }
      ]
    }
  ]
end

When new_record_path is omitted, Tramway looks for a configured entity using the association reflection's class name in underscore format. This means aliases such as has_many :responses, class_name: 'Comment' use the comment entity, not a response entity.

Generated association create paths include a redirect query parameter pointing to the current record's configured Tramway entity show path, for example: /comments/new?redirect=%2Fposts%2F1.

You can override the path explicitly when the associated record should be created through a custom route:

class PostDecorator < Tramway::BaseDecorator
  association :comments

  def show_associations
    [
      [
        :comments,
        { new_record_path: Rails.application.routes.url_helpers.new_post_comment_path(object) }
      ]
    ]
  end
end

Tramway still appends the redirect query parameter to custom paths. Associations without a create route and without new_record_path still render their records, but no creation button is displayed.

Decorate nil

Tramway Decorator does not decorate nil objects

user = nil
UserDecorator.decorate user # => nil

Update and Destroy

Read behave_as_ar section

Actions column on index tables

When an entity has update or destroy pages configured, Tramway automatically appends an Actions column to the end of the entity's index table headers, holding the edit/destroy controls for each row.

This label is looked up through I18n (tramway.table.actions_header), so you can rename it to your own language by overriding the key in your app's locale file:

config/locales/en.yml

en:
  tramway:
    table:
      actions_header: "Actions"

Tramway Form

Tramway provides convenient form objects for Rails applications. List properties you want to change and the rules in Form classes. No controllers overloading.

*app/forms/user_form.rb

class UserForm < Tramway::BaseForm
  properties :email, :password, :first_name, :last_name, :phone

  normalizes :email, with: ->(value) { value.strip.downcase }
end

Controllers without Tramway Form

app/controllers/users_controller.rb

class UsersController < ApplicationController
  def create
    @user = User.new
    if @user.save user_params
      render :show
    else
      render :new
    end
  end

  def update
    @user = User.find params[:id]
    if @user.save user_params
      render :show
    else
      render :edit
    end
  end
  
  private

  def user_params
    params[:user].permit(:email, :password, :first_name, :last_name, :phone)
  end
end

Controllers with Tramway Form

app/controllers/users_controller.rb

class UsersController < ApplicationController
  def create
    @user = tramway_form User.new
    if @user.submit params[:user]
      render :show
    else
      render :new
    end
  end

  def update
    @user = tramway_form User.find params[:id]
    if @user.submit params[:user]
      render :show
    else
      render :edit
    end
  end
end

We also provide submit! as save! method that returns an exception in case of failed saving.

Implement Form objects for any case

app/forms/user_updating_email_form.rb

class UserUpdatingEmailForm < Tramway::BaseForm
  properties :email
end

app/controllers/updating_emails_controller.rb

def update
  @user = UserUpdatingEmailForm.new User.find params[:id]
  if @user.submit params[:user]
    # success
  else
    # failure
  end
end

Create form namespaces

app/forms/admin/user_form.rb

class Admin::UserForm < Tramway::BaseForm
  properties :email, :password, :first_name, :last_name, :etc
end

app/controllers/admin/users_controller.rb

class Admin::UsersController < Admin::ApplicationController
  def create
    @user = tramway_form User.new, namespace: :admin
    if @user.submit params[:user]
      render :show
    else
      render :new
    end
  end

  def update
    @user = tramway_form User.find(params[:id]), namespace: :admin
    if @user.submit params[:user]
      render :show
    else
      render :edit
    end
  end
end

Normalizes

Tramway Form supports normalizes method. It's almost the same as in Rails

class UserForm < Tramway::BaseForme
  properties :email, :first_name, :last_name
  
  normalizes :email, with: ->(value) { value.strip.downcase }
  normalizes :first_name, :last_name, with: ->(value) { value.strip }
end

normalizes method arguments:

  • *properties - collection of properties that will be normalized
  • with: - a proc with a normalization
  • apply_on_nil - by default is false. When true Tramway Form applies normalization on nil values

Validations

Tramway Form supports validates method to add form-level validations before saving.

class UserForm < Tramway::BaseForm
  properties :email, :role

  validates :email, format: { with: /@/ }, allow_nil: true
  validates :role, presence: true
end

validates uses the standard ActiveModel/ActiveRecord validation options (for example presence: true, format:, length:, allow_nil, allow_blank, etc.), so with: is optional unless a validator requires it (like format).

When validations fail, submit returns false and adds errors to the form object, and submit! raises ActiveRecord::RecordInvalid.

Form inheritance

Tramway Form supports inheritance of properties, normalizations, validations, and fields.

Example

class UserForm < TramwayForm
  properties :email, :password

  normalizes :email, with: ->(value) { value.strip.downcase }
  validates :email, format: { with: /@/ }, allow_nil: true

  fields email: :email,
    password: :password
end

class AdminForm < UserForm
  properties :permissions
end

AdminForm.properties # returns [:email, :password, :permissions]
AdminForm.normalizations # contains the normalization of :email
AdminForm.validators_on(:email) # includes the validation of :email
AdminForm.fields # { email: :email, password: :password }

Make flexible and extendable forms

Tramway Form properties are not mapped to a model. You're able to make extended forms.

app/forms/user_form.rb

class UserForm < Tramway::BaseForm
  properties :email, :full_name

  # EXTENDED FIELD: full name
  def full_name=(value)
    object.first_name = value.split(' ').first
    object.last_name = value.split(' ').last
  end
end

Assign values

Tramway Form provides assign method that allows to assign values without saving

class UsersController < ApplicationController
  def update
    @user = tramway_form User.new
    @user.assign params[:user] # assigns values to the form object
    @user.reload # restores previous values
  end
end

Update and Destroy

Read behave_as_ar section

Tramway Navbar

Running tramway_navbar without arguments

When you call tramway_navbar without passing any arguments, it renders a navbar that lists buttons linking to all entities configured with a page index in config/initializers/tramway.rb.

Tramway provides DSL for rendering Tailwind Navgiation bar.

tramway_navbar title: 'Purple Magic', background: { color: :red, intensity: 500 } do |nav|
  nav.left do
    nav.item 'Users', '/users'
    nav.item 'Podcasts', '/podcasts'
  end

  nav.right do
    nav.item 'Sign out', '/users/sessions', method: :delete, confirm: 'Wanna quit?'
  end
end

ERB example

<%= tramway_navbar title: 'Purple Magic', background: { color: :red, intensity: 500 } do |nav| %>
  <% nav.left do %>
    <%= nav.item 'Users', '/users' %>
    <%= nav.item 'Podcasts', '/podcasts' %>
  <% end %>

  <% nav.right do %>
    <%= nav.item 'Sign out', '/users/sessions', method: :delete, confirm: 'Wanna quit?' %>
  <% end %>
<% end %>

will render this

Icons on navbar items

nav.item accepts an optional icon: option with a Font Awesome class string, rendered before the item's label:

nav.left do
  nav.item 'Users', '/users', icon: 'fa fa-users'
  nav.item 'Podcasts', '/podcasts', icon: 'fa fa-podcast'
end

Icons are optional — items without one just render as plain text links. When the vertical sidebar is collapsed on desktop, items rendered with an icon: stay visible and clickable as icon-only links (their text label is hidden but still available to screen readers via the item's aria-label); items without an icon have nothing to show once collapsed.

tramway_navbar

This helper provides several options. Here is YAML view of tramway_navbar options structure

title: String that will be added to the left side of the navbar
title_link: Link on Tramway Navbar title. Default: '/'
background:
  color: Css-color. Supports all named CSS colors and HEX colors
  intensity: Color intensity. Range: **100..950**. Used by Tailwind. Not supported in case of using HEX color in the background.color
with_entities: Show Tramway CRUD index page links to navbar. Default: true

When you use the default vertical navbar layout, it renders as a fixed left sidebar on desktop. The icon-only sidebar toggle collapses the whole sidebar and keeps the main content offset in sync, so the page shifts between md:pl-72 and md:pl-24 as the sidebar changes width.

The collapse/expand motion is tuned per element rather than a blanket transition-all: the sidebar width and the main content's padding-left each transition only that one property (transition-[width] / transition-[padding-left]) over 300ms on the ease-in-out-strong curve (cubic-bezier(0.77, 0, 0.175, 1), registered in config/tailwind.config.js), since both are on-screen movement. The header and nav-item list fade with transition-opacity on the ease-out-strong curve (cubic-bezier(0.23, 1, 0.32, 1)), matching that they're entering/exiting rather than moving. The toggle chevron no longer swaps between fa-chevron-left and fa-chevron-right — it stays a single fa-chevron-left icon that rotates 180° (transition-transform duration-200 ease-in-out-strong) to indicate state instead of snapping instantly. All of the above respect prefers-reduced-motion via motion-reduce:duration-0 on the movement/rotation transitions; the opacity fade is left intact since it aids comprehension without moving anything.

Tramway now ships with Font Awesome loaded through the engine, and the vertical navbar collapse control uses a single fa-chevron-left icon for both states (rotated when collapsed). Tramway ships a plain font-awesome.css asset for Propshaft compatibility and adds Font Awesome's font directory to the host app asset load path, so stylesheet_link_tag "font-awesome" resolves without extra app-side asset configuration. On mobile, tramway_navbar always renders the same top navbar regardless of direction:. Desktop :horizontal renders the classic top navbar, and desktop :vertical renders the collapsible sidebar.

The navbar is position: fixed (top-0 left-0 w-screen, upgraded to a md:fixed left sidebar of md:w-72 for the vertical desktop layout) at every breakpoint, so it stays put both when the page scrolls vertically and when a page with wide content (for example a wide table) scrolls horizontally — the navbar never drifts along with that scroll, including on mobile where position: sticky is unreliable across browsers for horizontal offsets. It uses w-screen (100vw) rather than w-full (100%) so its width always tracks the actual viewport instead of whatever containing block a 100% width would otherwise resolve against — the same overflowing wide content that makes the page scroll horizontally in the first place. Because the navbar is taken out of the page flow, tramway_main_container always reserves pt-16 at the top to match the fixed navbar's height on mobile (and on desktop for the :horizontal direction, where the top bar stays fixed at every breakpoint). When tramway_navbar renders before tramway_main_container, the main container helper keeps this in sync with direction:: for :vertical it clears that top padding at md: (md:pt-0) and adds the md:pl-72 left sidebar offset instead; for :horizontal it keeps the top padding and adds no left offset.

The collapsed/expanded state of the vertical sidebar persists across page navigations. The Stimulus controller saves the state to the browser's localStorage (key tramway-navbar-expanded) whenever the toggle button is used, and reads it back on every page load, so collapsing the sidebar on one page keeps it collapsed everywhere else until it's expanded again. This is per-browser state: it isn't synced across devices or persisted server-side, and if localStorage is unavailable (e.g. private browsing with storage blocked), the sidebar simply falls back to always starting expanded.

Since the server always renders the sidebar expanded (it has no way to know the browser's stored preference) and the Stimulus controller only applies the collapsed classes once it connects, tramway_navbar also renders a small blocking inline <script> alongside its markup. It reads localStorage synchronously, before the browser paints, and — if the sidebar was left collapsed — sets a data-tramway-navbar-expanded="false" attribute on <html>. A matching @media (min-width: 768px) CSS block (rendered alongside the navbar) keys off that attribute to apply the collapsed width, main-container padding, and faded header/menu immediately, before any JavaScript module has had a chance to load and run. This is what prevents the sidebar from flashing expanded and then snapping to collapsed on page load. The Stimulus controller keeps that <html> attribute in sync afterwards (on load and on every toggle), so this pre-paint styling and the controller's own classes never fight each other.

NOTE: tramway_navbar method called without arguments and block of code will render only Tramway CRUD links on the left.

In case you want to hide entity links you can pass with_entities: false.

The default :vertical navbar now renders as a fixed left sidebar on desktop screens. If your page content uses a full-width layout, wrap it with tramway_main_container class: 'md:pl-72' so the sidebar has room.

<% if current_user.present? %>
  <%= tramway_navbar title: 'WaiWai' do |nav| %>
    <% nav.left do %>
      <%= nav.item 'Board', admin_board_path %>
    <% end %>
  <% end %>
<% else %>
  <%= tramway_navbar title: 'WaiWai', with_entities: false %>
<% end %>

nav.left and nav.right

Tramway navbar provides left and right methods that puts items to left and right part of navbar.

nav.item

Item in navigation is rendered li a inside navbar ul tag on the left or right sides. nav.item uses the same approach as link_to method with syntax sugar.

tramway_navbar title: 'Purple Magic' do |nav|
  nav.left do
    nav.item 'Users', '/users'

    # NOTE you can achieve the same with

    nav.item '/users' do
      'Users'
    end

    # NOTE nav.item supports turbo-method and turbo-confirm attributes

    nav.item 'Delete user', '/users/destroy', method: :delete, confirm: 'Are you sure?'
    
    # will render this
    # <li>
    #   <a data-turbo-method="delete" data-turbo-confirm="Are you sure?" href="/users/sign_out" class="text-white hover:bg-red-300 px-4 py-2 rounded">
    #     Sign out
    #   </a>
    # </li>
  end
end

Tramway Flash

tramway_flash renders the dark shadcn-style flash component that Tramway uses in its layouts. Pass the flash text and type, and the helper will resolve the proper dark semantic accent (for example :success -> green, :warning -> orange). Tramway does not render a separate light flash theme. You can also provide custom HTML options directly (e.g., class:, data:) and they will be merged into the flash container.

-# Haml example
= tramway_flash text: flash[:notice], type: :hope
= tramway_flash text: 'Double check your data', type: :greed, class: 'mt-2', data: { turbo: 'false' }
<%# ERB example %>
<%= tramway_flash text: flash[:alert], type: :rage %>
<%= tramway_flash text: 'Saved!', type: :will, data: { controller: 'dismissible' } %>

Use the type argument is compatible to Lantern Color Palette or provide a color: keyword to set the semantic accent explicitly.

Tramway Tooltip

tramway_tooltip renders a dark tooltip around block content with a default minimum width of min-w-40 and maximum width of max-w-sm. Pass text: for the tooltip body and event: to choose when it appears. Supported events are :hover and :onclick; hover is the default.

= tramway_tooltip text: 'Shown on hover' do
  %span Help

= tramway_tooltip text: 'Shown after click', event: :onclick do
  %button More info
<%= tramway_tooltip text: 'Shown on hover' do %>
  <span>Help</span>
<% end %>

<%= tramway_tooltip text: 'Shown after click', event: :onclick do %>
  <button>More info</button>
<% end %>

Tramway Chat

tramway_chat renders the chat experience bundled with Tramway. Provide a chat ID, a list of message hashes, and the URL that receives new messages. Each message must include an :id and a :type (either :sent or :received). Additional message fields like text, data, or sent_at are forwarded to tramway/chats/message_component. Message text preserves normal word wrapping and only breaks oversized words or links when needed to keep content inside the message bubble.

Use send_messages_enabled: to control whether users can send new messages from the rendered form. It defaults to true. When set to false, the text field is disabled and the waiting placeholder is shown.

-# Haml example
- messages = [
-  { id: 1, type: :received, text: 'Hello 👋' },
-  { id: 2, type: :sent, text: 'Hi there!' }
- ]
= tramway_chat chat_id: 'support-chat',
  messages: messages,
  message_form: @message_form,
  send_message_path: chat_messages_path,
  send_messages_enabled: @chat.open?
<%# ERB example %>
<% messages = [{ id: 1, type: :received, text: 'Hello 👋' }] %>
<%= tramway_chat chat_id: 'support-chat',
  messages: messages,
  message_form: @message_form,
  send_message_path: chat_messages_path,
  send_messages_enabled: @chat.open? %>

If you do not want to render the message form, pass message_form: nil. When the form is present, send_message_path is required and the helper will generate the correct POST form.

To append a message to an already-rendered tramway_chat stream, use tramway_chat_append_message. The method is mixed into controllers and ActiveRecord models by Tramway and expects:

  • chat_id: — the same value used in tramway_chat chat_id:
  • type: — only :sent or :received (raises ArgumentError otherwise)
  • text: — message content
  • sent_at: — message timestamp
tramway_chat_append_message(
  chat_id: 'support-chat',
  type: :received,
  text: 'We got your request',
  sent_at: Time.current
)

It broadcasts with target: 'messages' and renders the tramway/chats/message partial so the message appears in the live chat.

To prepend a message to the same stream, use tramway_chat_prepend_message. It accepts the same arguments and validation rules, but inserts the rendered tramway/chats/message partial at the beginning of the messages target instead of the end.

tramway_chat_prepend_message(
  chat_id: 'support-chat',
  type: :received,
  text: 'Earlier message from history',
  sent_at: 5.minutes.ago
)

To append multiple messages at once, use tramway_chat_append_messages. The method expects:

  • chat_id: — the same value used in tramway_chat chat_id:
  • messages: — an array of hashes, where each hash includes type:, text:, and sent_at:

Each message type: must be :sent or :received or the helper raises ArgumentError. It broadcasts with target: 'messages' and renders the tramway/chats/messages partial so the full collection appears in the live chat.

tramway_chat_append_messages(
  chat_id: 'support-chat',
  messages: [
    { type: :received, text: 'First update', sent_at: 2.minutes.ago },
    { type: :sent, text: 'Thanks, checking now', sent_at: 1.minute.ago }
  ]
)

To prepend multiple messages, use tramway_chat_prepend_messages. It accepts the same chat_id: and messages: arguments as tramway_chat_append_messages, validates each type:, and prepends the rendered tramway/chats/messages partial to the beginning of the messages target.

tramway_chat_prepend_messages(
  chat_id: 'support-chat',
  messages: [
    { type: :received, text: 'Older history item', sent_at: 10.minutes.ago },
    { type: :sent, text: 'Reply from earlier', sent_at: 9.minutes.ago }
  ]
)

Tramway Table Component

Tramway provides a responsive, tailwind-styled table with light and dark themes. Use the tramway_table, tramway_row, and tramway_cell helpers to build tables with readable ERB templates while still leveraging the underlying ViewComponent implementations.

<%= tramway_table do %>
  <%= tramway_header headers: ['Column 1', 'Column 2'] %>

  <%= tramway_row do %>
    <%= tramway_cell do %>
      Something
    <% end %>
    <%= tramway_cell do %>
      Another
    <% end %>
  <% end %>
<% end %>

tramway_table accepts an optional size: argument plus the same options hash as Tramway::TableComponent. Supported sizes are :small, :medium, and :large. The default is :medium, and it renders exactly like the current table implementation. The options hash is forwarded as HTML attributes, so you can pass things like id, data attributes, or additional classes. The table itself carries the default dark background (bg-zinc-950), so you can override it directly with a utility like class: '!bg-red-500'. If you do not supply your own width utility (e.g. a class that starts with w-), the component automatically appends w-full to keep the table responsive.

tramway_row, tramway_header, and tramway_cell automatically follow the active table size, so a single size: on tramway_table controls the whole table.

Use the optional href: argument on tramway_row to turn an entire row into a link. Linked rows keep their transparent background and gain pointer and hover styles (cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700) to indicate interactivity.

<%= tramway_table size: :large, class: 'max-w-3xl border border-gray-200', data: { controller: 'table' } do %>
  <%= tramway_header headers: ['Name', 'Email'] %>

  <%= tramway_row href: user_path(user) do %>
    <%= tramway_cell do %>
      <%= user.name %>
    <% end %>
    <%= tramway_cell do %>
      <%= user.email %>
    <% end %>
  <% end %>
<% end %>

Cell content that does not fit its column is truncated with an ellipsis. This is a pure CSS behavior (truncate + min-w-0 on each cell) — no JavaScript is involved, and no configuration is needed to enable it.

Pass truncate: false to tramway_cell to opt a specific cell out of this behavior and let its content wrap/overflow normally:

<%= tramway_cell truncate: false do %>
  <%= user.bio %>
<% end %>

On narrow screens all columns always render, and the table scrolls horizontally within itself (the page never stretches past the viewport width) so every column stays reachable. See docs/users/tramway_table.md for the full details of the mobile scroll behavior.

Tramway Grid

Use tramway_grid when you want dashboard-style layouts with cards that span multiple rows and columns while keeping the existing Tramway dark zinc styling. The grid accepts rows: and columns:, and it also accepts outline: true when you want to show separators between cells. Those separators are drawn in the gap between grid cells, so they stay centered when cards have content. Cards use size: [rows, columns], with responsive spans on desktop and stacked cards on smaller screens. Each grid cell is rendered as a real 20em x 20em box, and the grid always builds the exact rows x columns matrix even when the slot is empty. Cards participate in the same grid, so size: [1, 1] occupies exactly one cell.

= tramway_grid rows: 8, columns: 8, outline: true do
  = tramway_card size: [1, 2] do
    .text-sm.font-medium.text-zinc-400 Revenue
    .mt-2.text-3xl.font-semibold.text-zinc-50 $128,240

  = tramway_card size: [2, 3] do
    .text-sm.font-medium.text-zinc-400 Activity
    .mt-4.space-y-2
      .flex.items-center.justify-between
        %span.text-zinc-300 Orders
        %span.text-zinc-50 48
      .flex.items-center.justify-between
        %span.text-zinc-300 Refunds
        %span.text-zinc-50 3

tramway_cell also forwards any HTML options to the underlying cell wrapper. Use this when you need per-cell classes, data attributes, ids, or other attributes:

<%= tramway_cell class: 'whitespace-nowrap', data: { turbo: false } do %>
  <%= user.name %>
<% end %>

tramway_header forwards HTML options to the header wrapper as well. That makes it easy to add ids, classes, or data attributes to the header row:

<%= tramway_header headers: ['Name', 'Email'], class: 'sticky top-0', data: { controller: 'table-header' } %>

When you render a header you can either pass the headers: array, as in the examples above, or render custom header content in the block. tramway_header uses the length of the headers array to build the grid if the array is present. If you omit the array and provide custom content, pass the columns: argument so the component knows how many grid columns to generate.

<%= tramway_header columns: 4 do %>
  <%= tramway_cell do %>
    Custom header cell
  <% end %>
  <%= tramway_cell do %>
    Another header cell
  <% end %>
  <!-- ... -->
<% end %>

With this approach you control the header layout while still benefiting from the default Tailwind grid classes that the header component applies.

Tramway Buttons and Containers

Tramway ships with helpers for common UI patterns built on top of Tailwind components.

tramway_button helper is designed for developers who don't want to think about what type of button must be used now. It can render 3 types of buttons: a (links), button, form with button.

You can set a type to render explicitly:

tramway_button tag: :a, text: 'Link' #=> <a class=BUTTON_CLASSES>Link</a>
tramway_button tag: :button, text: 'Link' #=> <button class=BUTTON_CLASSES>Link</button>
tramway_button tag: :form, text: 'Link' #=> <form><button class=BUTTON_CLASSES>Link</button></form>

OR tramway_button choose the most appropriate button type by the arguments received.

tramway_button options

path

Example 1: rendering link

<%= tramway_button path: '/projects', text: 'Projects' %>

#=> <a href='/projects' class="...">Projects</a>

Example 2: rendering form

<%= tramway_button path: '/projects/1', text: 'Destroy', method: :delete %>

#> <form action="/projects/1"  method="post">
#>   ....
#>   <button class: "..." type="submit">
#>     Delete
#>   </button>

Example 3: rendering button

<%= tramway_button path: '/projects/1', text: 'Edit', tag: :button %>

#> <button onclick="window.location.href='/projects/1' class="...">Edit</button>

All additional keyword arguments are forwarded to the underlying component as HTML attributes.

You can also pass HTML attributes for the generated <form> separately via form_options: while keeping button-specific attributes in options::

<%= tramway_button path: user_path(user), text: 'Create',
  form_options: { data: { turbo: false } },
  options: { data: { controller: 'submit-once' } } %>

The type option maps semantic intent to Lantern Color Palette.

Use size: to select one of the built-in button sizes: :small, :medium, or :large. The default is :medium.

<%= tramway_button path: '/projects', text: 'Projects', size: :small %>

Use tooltip: to show Tramway's tooltip from the rendered button. The tooltip accepts the same event: values as tramway_tooltip: :hover or :onclick.

<%= tramway_button path: '/projects', text: 'Projects', tooltip: { text: 'Open projects', event: :hover } %>
  • tramway_badge renders a dark shadcn-style Tailwind badge with the provided text. Pass a semantic type (for example, :success or :danger) to use the built-in color mappings, or supply a custom Tailwind color family with color:. When you opt into a custom color, ensure the corresponding accent utilities are available in your Tailwind safelist.

  • tramway_container (backed by Tramway::Containers::NarrowComponent, used for example by the entities index page) now sizes itself with w-max min-w-full instead of w-full on both its outer box and inner flex wrapper. A plain block box with w-full never grows past 100% of its parent even when a child (e.g. a wide table forcing horizontal scroll) overflows it, so the container's background stops at the viewport edge while the overflowing content keeps going. w-max sizes the box to its content's intrinsic (max-content) width, and min-w-full keeps a 100% floor so normal, non-overflowing pages still span the full width — together the container always covers the whole page, even when something inside it stretches wider than the viewport.

    <%= tramway_badge text: 'Active', type: :success %>
  • tramway_back_button renders a standardized "Back" link.

    <%= tramway_back_button %>
  • tramway_container wraps content in a responsive, narrow layout container. Pass an id if you need to target the container with JavaScript or CSS.

    <%= tramway_container id: 'user-settings' do %>
      <h2 class="text-xl font-semibold">Settings</h2>
      <p class="mt-2 text-gray-600">Update your preferences below.</p>
    <% end %>

Tailwind-styled forms

Tramway uses Tailwind by default. All UI helpers are implemented with ViewComponent.

tramway_form_for

Tramway provides tramway_form_for helper that renders Tailwind-styled forms by default. Form inputs use hardcoded dark shadcn-style classes; Tramway does not render a separate light form theme. Checkboxes render with the same dark border and focus highlight treatment as other inputs, then use the light primary checked state. Checkbox controls, checkmarks, and vertically centered label line heights scale together with the form-level size: option.

<%= tramway_form_for @user do |f| %>
  <%= f.text_field :text %>
  <%= f.email_field :email %>
  <%= f.password_field :password %>
  <%= f.select :role, [:admin, :user] %>
  <%= f.date_field :birth_date %>
  <%= f.datetime_field :confirmed_at %>
  <%= f.rich_text_area :bio %>
  <%= f.tramway_select :permissions, [['Create User', 'create_user'], ['Update user', 'update_user']] %>
  <%= f.file_field :file %>
  <%= f.submit 'Create User' %>
<% end %>

will render this

Use size: to control the input sizing (:small, :medium, or :large). The default is :medium, and supported inputs rendered within the form will use the same size value. Date and datetime fields open the browser's native picker when clicking anywhere in the input, not only the picker icon. Use hint: on any field to render muted helper text below the input. The hint option is handled by Tramway and is not forwarded as an HTML attribute.

<%= tramway_form_for @user, size: :large do |f| %>
  <%= f.text_field :text, hint: 'This name is visible to teammates.' %>
  <%= f.submit 'Create User' %>
<% end %>

Use horizontal: true to render a horizontal form layout.

<%= tramway_form_for @user, horizontal: true do |f| %>
  <%= f.text_field :text %>
  <%= f.submit 'Create User' %>
<% end %>

Available form helpers:

  • text_field
  • email_field
  • number_field
  • text_area
  • password_field
  • file_field
  • check_box
  • checkbox (alias for check_box)
  • select
  • date_field
  • datetime_field
  • time_field
  • rich_text_area (Action Text/Trix editor with Tramway dark form classes)
  • tramway_select (Stimulus-based)
  • submit

Autocomplete select example:

<%= tramway_form_for @user do |f| %>
  <%= f.select :role, [:admin, :user], autocomplete: true %>
<% end %>

autocomplete: true renders an autocomplete-enabled select. It can be combined with multiple: true to get a searchable multi-select:

<%= tramway_form_for @user do |f| %>
  <%= f.select :departments, [['Engineering', 'engineering'], ['Sales', 'sales']], autocomplete: true, multiple: true %>
<% end %>

With both options set, the search input stays visible after each selection so you can keep filtering and picking more options; selected options still show as removable tags.

When you use multiple: true, make sure any preselected values still exist in the collection you render. Unknown values are ignored by the widget so the select can still boot safely.

Examples

  1. Sign In Form for devise authentication

app/views/devise/sessions/new.html.erb

<%= tramway_form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: 'bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4') } do |f| %>
  <%= component 'forms/errors', record: resource %>

  <%= f.text_field :email, placeholder: 'Your email' %>
  <%= f.password_field :password, placeholder: 'Your password' %>

  <%= f.submit 'Sign In' %>
<% end %>
  1. Sign In Form for Rails authorization

app/views/sessions/new.html.erb

<%= form_with url: login_path, scope: :session, local: true, builder: Tramway::Form::Builder do |form| %>
  <%= form.email_field :email %>
  <%= form.password_field :password %>
  <%= form.submit 'Log in' %>
<% end %>

Rich text area

rich_text_area (and its alias rich_textarea) renders a Trix rich text editor with Tramway's dark-theme Tailwind styling. Trix is bundled with Rails and does not require an additional gem.

Before using rich_text_area, the Trix stylesheet and JavaScript must be present in your application layout. The tramway:install generator adds them automatically. To add them manually:

-# app/views/layouts/application.html.haml
= stylesheet_link_tag "trix", "data-turbo-track": "reload"
= javascript_include_tag "trix", "data-turbo-track": "reload", defer: true
<%# app/views/layouts/application.html.erb %>
<%= stylesheet_link_tag "trix", "data-turbo-track": "reload" %>
<%= javascript_include_tag "trix", "data-turbo-track": "reload", defer: true %>

Basic usage:

<%= tramway_form_for @post do |f| %>
  <%= f.rich_text_area :body %>
<% end %>

Custom label and extra CSS class:

<%= tramway_form_for @post do |f| %>
  <%= f.rich_text_area :body, label: 'Post body', class: 'min-h-60' %>
<% end %>

Suppress the label:

<%= tramway_form_for @post do |f| %>
  <%= f.rich_text_area :body, label: false %>
<% end %>

Pass data attributes for Stimulus controllers:

<%= tramway_form_for @post do |f| %>
  <%= f.rich_text_area :body, data: { controller: 'mentions' } %>
<% end %>

size: is a Tramway-level option and is not forwarded to the <trix-editor> element.

Stimulus-based inputs

tramway_form_for provides Tailwind-styled Stimulus-based custom inputs.

Tramway Select

In case you want to use the tailwind-styled Tramway select this way

<%= tramway_form_for @user do |f| %>
  <%= f.tramway_select :permissions, [['Create User', 'create_user'], ['Update user', 'update_user']] %>
  <%# ... %>
<% end %>

you should add the Tramway Select Stimulus controller to your application.

Example for importmap-rails config

config/importmap.rb

pin '@tramway/tramway', to: 'tramway/tramway.js'

app/javascript/controllers/index.js

import { application } from "controllers/application"
import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
import { TramwaySelect } from "@tramway/tramway" // importing TramwaySelect controller class
eagerLoadControllersFrom("controllers", application)

application.register('tramway-select', TramwaySelect) // register TramwaySelect controller class as `tramway-select` stimulus controller

In case you need to use Stimulus change action with Tramway Select

<%= tramway_form_for @user do |f| %>
  <%= f.tramway_select :role, data: { action: 'change->user-form#updateForm' } %>
<% end %>

Remote form example:

<%= tramway_form_for @user, remote: true do |f| %>
  <%= f.text_field :name %>
  <%= f.email_field :email %>
<% end %>

With remote: true, Tramway submits the form on each input change via inline JavaScript; no additional controller setup is required.

Tailwind-styled pagination for Kaminari

Tramway uses Tailwind by default. It has tailwind-styled pagination for kaminari.

How to use

app/views/users/index.html.erb

<%= paginate @users %> <%# it will render tailwind-styled pagination buttons by default %>

Pagination buttons looks like this

On small screens (below Tailwind's sm breakpoint), the "First" and "Last" buttons are hidden and only the current page number stays visible next to the Prev/Next arrows, so the control doesn't overflow a phone-width viewport. Pair it with Kaminari's own page_entries_info helper to show a compact entry count on mobile instead of the full set of page links:

= paginate @users
%p.sm:hidden
  = page_entries_info @users

behave_as_ar

Tramway Decorator and Tramway Form support behave_as_ar method. It allows to use update and destroy methods with decorated and form objects.

object method

Tramway Decorator and Tramway Form have public object method. It allows to access ActiveRecord object itself.

user_1 = tramway_decorate User.first
user_1.object #=> returns pure user object

user_2 = tramway_form User.first
user_2.object #=> returns pure user object

Configuration

Custom layout

In case you wanna use a custom layout:

  1. Create a controller
  2. Set the layout there
  3. Set this controller as application_controller in Tramway initializer
  4. Reload your server

Example

app/controllers/admin/application_controller.rb

class Admin::ApplicationController < ApplicationController
  layout 'admin/application'
end

config/initializers/tramway.rb

Tramway.configure do |config|
  config.application_controller = 'Admin::ApplicationController'
end

Plugins

Plugins add optional, ready-made dashboards to your app, mounted and styled with Tramway. Enable one by name and, optionally, configure it:

Tramway.configure do |config|
  config.plugins = [:solid_queue]
  config.plugins.solid_queue = {
    path: '/jobs'
  }
end

Enabling a plugin also adds a navbar entry for it automatically (see Tramway Navbar).

SolidQueue

The :solid_queue plugin is a dashboard for inspecting and managing SolidQueue jobs. It requires the solid_queue gem in your Gemfile — if it's missing, Tramway raises a clear error explaining how to fix it instead of failing deep inside a controller.

It gives you:

  • A paginated, filterable jobs list (by status, queue, class name, or free-text search by class name/active job ID/numeric ID), with per-status counts.
  • A job detail page, including failure details (exception class, message, backtrace) for failed jobs.
  • Retry, discard and destroy for a single job, or in bulk for a selection of jobs — or "Destroy all" to destroy every job matching the currently active filters at once. Bulk actions and "Destroy all" run in the background (you'll see a flash message confirming the action was queued, not completed instantly) on their own dedicated worker queue, which tramway:install sets up for you.
  • A queues list with size/running/latency/throughput, and pause/resume/clear actions per queue. "Size" only counts jobs waiting to run — "Running" shows jobs currently executing on that queue, so a queue processing one big job (e.g. a large "Destroy all") doesn't look idle just because nothing is waiting behind it.
  • A recurring tasks list with the ability to enqueue a task immediately.
  • Jobs, queues, and recurring tasks lists (and the job detail page) refresh themselves in the background every few seconds, so job status changes show up without reloading the page.

See docs/users/tramway_solid_queue_plugin.md for more on what "discard" does and other SolidQueue-specific behavior.

Lantern Color Palette

Type Color
default, life Gray
primary, hope Blue
secondary Zinc
success, will Green
warning, greed Orange
danger, rage Red
love Violet
compassion Indigo
fear Yellow

Articles

Contributing

lefthook is pinned in .tool-versions so mise can resolve the shim used by make install.

make install

License

The gem is available as open source under the terms of the MIT License.

About

Start your Rails project with Tramway

Resources

Stars

31 stars

Watchers

3 watching

Forks

Releases

Packages

Used by

Contributors

Languages