Build custom UI inside an Avo admin panel and wire up the JS/CSS/Tailwind plumbing behind it — standalone custom-tool pages (route + controller + sidebar item), resource tools (a partial embedded in a record's Show/Edit view, including extra form inputs and nested forms), Avo's native ViewComponents (a_button/a_link, ui.panel, ui.card, ui.description_list) for consistent, dark-mode-ready markup, ejecting Avo's own partials/components/controllers to customize them, Stimulus controllers and dynamic/dependent forms, adding your own CSS/JS through the asset pipeline, the TailwindCSS integration, and packaging any of it as an Avo plugin. Use when the user wants to add a custom page/section to the admin, build a dashboard widget or custom panel on a record page, add extra inputs to an edit form that aren't columns, build a nested form, make a form dynamic (when country changes reload the cities — cascading/dependent selects), toggle or disable a field based on another field, add their own JavaScript or CSS or a Sti
Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.
Quelldateien prüfen
Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.
Mit Codex oder Claude installieren Kopieren Sie diesen Prompt, fügen Sie ihn in Codex, Claude oder einen anderen Assistant ein und lassen Sie die Skill-Seite prüfen und installieren.
Ein direkter Befehl überspringt den Prüf-Prompt. Prüfen Sie die Quelle, bevor Sie ihn ausführen.
Der Befehl bleibt in einer Zeile. Scrollen Sie horizontal, um ihn vor dem Kopieren vollständig zu prüfen.
Sie bevorzugen eine lokale Kopie? Laden Sie die Dateien herunter, die SkillsMP derzeit vorliegen.
SKILL.md wird angezeigt
SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
name
avo-custom-ui
description
Build custom UI inside an Avo admin panel and wire up the JS/CSS/Tailwind plumbing behind it — standalone custom-tool pages (route + controller + sidebar item), resource tools (a partial embedded in a record's Show/Edit view, including extra form inputs and nested forms), Avo's native ViewComponents (a_button/a_link, ui.panel, ui.card, ui.description_list) for consistent, dark-mode-ready markup, ejecting Avo's own partials/components/controllers to customize them, Stimulus controllers and dynamic/dependent forms, adding your own CSS/JS through the asset pipeline, the TailwindCSS integration, and packaging any of it as an Avo plugin. Use when the user wants to add a custom page/section to the admin, build a dashboard widget or custom panel on a record page, add extra inputs to an edit form that aren't columns, build a nested form, make a form dynamic (when country changes reload the cities — cascading/dependent selects), toggle or disable a field based on another field, add their own JavaScript or CSS or a Stimulus controller to the admin, override how Avo renders the index table or any view's markup, style a custom tool so it matches the rest of the admin, use Tailwind classes in custom admin UI, migrate the admin's styling to Tailwind 4, or package their customization as a reusable plugin/gem — whether they phrase it with Avo terms ("custom tool", "resource tool", "eject", "ui.panel", "stimulus_controllers") or as a plain Rails/product ask ("add a custom admin page", "embed a widget on the show page", "extra fields on the form", "dependent dropdowns in the admin", "add JS to my admin").
allowed-tools
Read, Edit, Write, Glob, Grep, Bash, WebFetch
metadata
{"requires-gem":"none — Community"}
Avo Custom UI
Everything about building your own UI inside an Avo admin and the asset plumbing behind it: standalone pages, embedded record widgets, Avo's reusable components, ejecting Avo's markup, Stimulus/dynamic forms, custom CSS/JS, the Tailwind integration, and packaging it all as a plugin.
Two things to fix before writing any code:
Which surface? A standalone page (own route + sidebar item) is a custom tool. A widget on an existing record's Show/Edit is a resource tool. Rendering Avo's existing markup differently is an eject. A new field type is a different skill (see Gotchas).
Everything here is Community. Custom tools, resource tools, ejecting, JS/Stimulus, and the Tailwind integration need no paid gem.
Docs — fetch on demand with WebFetch; prefer the raw .md (clean, no HTML). Read the page before implementing anything non-trivial in that area.
Related skills: re-skinning colors/spacing through CSS variables is avo-branding-appearance; defining a brand-new field type is avo-custom-fields; enabling a registered custom index view type on a resource is avo-index-views.
Custom tools
A standalone page — its own route, controller action, sidebar item — rendered inside Avo's layout. Reach for it for dashboards, reports, admin utility screens.
Generate
bin/rails generate avo:tool dashboard
Creates (and restarts the server so the route takes effect):
app/views/avo/sidebar/items/_dashboard.html.erb — the sidebar link (all files in this dir load into the sidebar, alphabetically — rename to reorder)
app/controllers/avo/tools_controller.rb — a ToolsController on first run, with the action inserted
app/views/avo/tools/dashboard.html.erb — the page view
a route injected inside your mount_avo block in config/routes.rb
Controller + route
# app/controllers/avo/tools_controller.rbclassAvo::ToolsController < Avo::ApplicationControllerdefdashboard@page_title = "Dashboard"# rendered via the meta-tags gem
add_breadcrumb title:"Dashboard"endend
# config/routes.rb — injected inside mount_avo, so it inherits Avo's auth
authenticate :user, ->(user) { user.is_admin? } do
mount_avo do
get "dashboard", to:"tools#dashboard", as::dashboardendend
Because the action inherits from Avo::ApplicationController, the view runs with the full Avo view context: ui.* components, @page_title and any instance var you set, _current_user, Avo::Current.context, params, and the avo. / main_app. path helpers. Set instance variables in the action, read them in the view — normal Rails.
View
Build the page from Avo's own components so it matches the admin and gets dark mode for free:
<%# app/views/avo/tools/dashboard.html.erb %>
<div class="flex flex-col">
<%= render ui.panel(title: "Dashboard") do |panel| %>
<% panel.with_controls do %>
<%= a_link("/admin", icon: "tabler/outline/external-link", style: :primary, color: :primary) do %>
Admin
<% end %>
<% end %>
<% panel.with_card(title: "New tool", padded: true) do %>
<div class="flex flex-col justify-between min-h-24 space-y-4">
<h3>What a nice new tool 👋</h3>
</div>
<% end %>
<% end %>
</div>
Use your app's helpers / path helpers
classAvo::ToolsController < Avo::ApplicationController
helper HomeHelper# make your helpers available in the viewdefdashboard@page_title = "Dashboard"endend
Inside the engine, prefix path helpers: avo.resources_posts_path(1) for Avo routes, main_app.posts_path for your app's routes. Load your own CSS/JS via Assets & Tailwind.
Resource tools
A partial embedded inside a resource's Show/Edit view — a widget bound to one record. Default-visible on Show.
Generate + register
bin/rails generate avo:resource_tool post_info
Creates the config class app/avo/resource_tools/post_info.rb and the partial app/views/avo/resource_tools/_post_info.html.erb. Register it in the resource's fields block (it's placed like a field):
# app/avo/resources/post.rbclassAvo::Resources::Post < Avo::BaseResourcedeffields
tool Avo::ResourceTools::PostInfo, show_on::edit# or :forms, or default (Show)endend
Control placement with the usual field visibility options (show_on, only_on, show_on: :forms).
Partial context
The partial has access to: tool (your PostInfo instance), @resource (with .record, .view, .params), form (only on New/Edit — check form.present?), params, Avo::Current.context, and current_user.
Keep logic out of the view
Define post_initialize on the tool — Avo calls it after hydration (no super, no overriding initialize). Inside it you have resource, parent, and view. Expose data via readers/methods and read them through tool in the partial:
classAvo::ResourceTools::PostInfo < Avo::BaseResourceToolself.name = "Post info"# self.partial = "avo/resource_tools/post_info" # override the partial path if neededattr_reader:foodefpost_initialize@foo = :barenddefcustom_method_call = :calledend
<%= tool.foo %>
<%= tool.custom_method_call %>
Custom form inputs and nested forms
A resource tool is the way to add inputs to a form that aren't backed by columns — including nested (fields_for) inputs producing arrays/Hashes. Three steps, all required:
Render the inputs against form in the partial (show it only on forms):
<%# app/views/avo/resource_tools/_fish_information.html.erb %>
<%= render ui.panel(title: @resource.record.name) do |panel| %>
<% panel.with_card(padded: true) do %>
<% if form.present? %>
<%= form.label :fish_type %>
<%= form.text_field :fish_type, class: input_classes %>
<%= form.label :properties %>
<%= form.text_field :properties, multiple: true, class: input_classes %> <%# array %>
<% form.fields_for :information do |information_form| %>
<%= information_form.text_field :name, class: input_classes %> <%# Hash %>
<%= information_form.text_field :history, class: input_classes %>
<% end %>
<% end %>
<% end %>
<% end %>
Permit the params on the resource via self.extra_params (Avo runs model.assign_attributes params.permit(extra_params)). Anything not listed here silently never reaches the model.
Make the model respond to those attributes (real columns, or setters like def fish_type=(v)).
Native components
Use Avo's own ViewComponents instead of hand-rolled <div>s — they keep custom UI visually consistent and dark-mode-ready for free. All are reachable in any Avo view context (custom tools, resource tools, ejected partials).
a_button / a_link → Avo::ButtonComponent. a_button renders a <button>, a_link an <a> (first arg = href). Options: style: (:primary/:outline/:text, default :outline), size: (:xs/:sm/:md/:lg), color: (:primary, :accent, or any Tailwind color), icon: / end_icon: (icon path), rounded: :full (pill). Extra kwargs pass through to link_to/button_to — method:, data:, title:.
ui.panel(...) → Avo::UI::PanelComponent. A titled container with slots: with_controls (right-aligned header buttons), with_card (bordered card body), with_body (flush body, no card), plus with_cover, with_sidebar, with_pre_body, with_footer, with_header. Options: title:, description:, class:, data:.
ui.card(...) → Avo::UI::CardComponent. A standalone bordered surface (the same card panel.with_card wraps). Options: title:, description:, padded:, class: (modifiers: card--padded, card--compact-wrapper, card--compact-header-y, card--compact-header-x — header compaction is split per axis, so compose the last two for both). Slots: with_header, with_body, with_footer.
ui.description_list → wrap a list of fields so they render full-width with dividers, exactly like Avo's own field lists.
Prefer built-in options over utility classes.padded: true gives the card body Avo's exact standard padding — reach for it instead of adding your own px-*/py-*. title: / description: build the header for you. Inside a ui.panel, use its with_card slot rather than nesting a ui.card yourself. The card body ships unpadded by default (so tables/scrollers sit flush) — opt into padded: true for free-form content like forms and prose.
Eject views
Avo renders from partials, ViewComponents, and controllers inside the gem. When config isn't enough, copy one into your app and edit it — your copy takes precedence.
# A gem partial → same path in your app
bin/rails generate avo:eject --partial app/views/layouts/avo/application.html.erb
# A prepared-template shorthand (ejects to app/views/avo/partials/)
bin/rails generate avo:eject --partial :logo
bin/rails generate avo:eject --partial :head# inside <head>, after Avo's assets — style overrides
bin/rails generate avo:eject --partial :pre_head # inside <head>, before Avo's assets
bin/rails generate avo:eject --partial :sidebar_extra
# A ViewComponent (copies both .rb and .html.erb)
bin/rails generate avo:eject --component Avo::Index::TableRowComponent
# All of a field's components, or just one view
bin/rails generate avo:eject --field-components text
bin/rails generate avo:eject --field-components text --view edit
# A controller (most commonly the application_controller extension layer)
bin/rails generate avo:eject --controller application_controller
Prepared-template symbols: :logo, :header, :pre_head, :head, :scripts, :sidebar_extra, :profile_menu_extra, plus the override files :avo_overrides_css, :avo_overrides_js, :asset_overrides.
Scope to avoid replacing a component everywhere. By default an ejected Avo::Views / Avo::Fields component replaces the original across the whole app. Add --scope to nest your copy in its own namespace, then wire it in only where wanted via self.components (resource) or the components: field option:
Avo integrates a light Stimulus layer so you can make forms dynamic. First, add your JS entrypoint through the asset pipeline — nothing loads otherwise.
Avo also adds a default resource-edit / resource-show / resource-index controller per view, and passes each controller a view value (index/show/edit/new) in the DOM. For every field it emits ready-made Stimulus targets you can hook into:
#toggle hides/shows the target; #disable greys it out; #debugOnInput logs events to the console for targeting checks. Target a ...Wrapper when the field has multiple inputs (e.g. polymorphic belongs_to).
Dependent selects (cascading dropdowns)
The canonical "change country → repopulate cities" flow. Wire an input-> action + targets on the two selects, add an Avo engine route + controller action that returns JSON, and register a custom Stimulus controller that fetches and repopulates on connect and on change:
# config/routes.rb — inside the Avo engineAvo::Engine.routes.draw do
scope :resourcesdo
get "courses/cities", to:"courses#cities"endend# app/controllers/avo/courses_controller.rbclassAvo::CoursesController < Avo::ResourcesControllerdefcities = render json:Course.cities[params[:country].to_sym] || []
end
The Stimulus controller fetches ${window.Avo.configuration.root_path}/resources/courses/cities?country=... and rebuilds the city <option>s. Fetch the JavaScript doc for the full controller — it captures the initial value, guards to edit/new via viewValue, and shows a loading overlay.
Register a custom controller
In your entrypoint, hook Avo's Stimulus instance and register your controller:
Avo hooks into your app's existing pipeline — Importmap, esbuild/jsbundling, Propshaft, and Sprockets are all fully supported. The generator does the wiring:
bin/rails generate avo:js:install # Importmap: creates app/javascript/avo.custom.js,# ejects _head.html.erb to load it, pins it in importmap.rb
bin/rails generate avo:js:install --bundler esbuild # jsbundling/esbuild entrypoint instead
Manual (Sprockets/Propshaft): eject :pre_head, create avo.custom.js + avo.custom.css, and load them (javascript_include_tag "avo.custom", defer: true — always defer: true so order matches Avo's).
Load order in <head> (later wins the cascade):_pre_head (yours) → Avo's own CSS/JS → avo-overrides.css / avo-overrides.js → _head (yours) → brand-palette overrides. Put styles in _pre_head to let Avo's defaults load after them; use _head when you deliberately want to win.
Tailwind integration
When Avo detects tailwindcss-ruby, it auto-enables the Tailwind integration and compiles an app-level stylesheet (app/assets/builds/avo/application.css) that includes Avo core + plugin styles, your styles from app/assets/stylesheets/avo/**/*.css, and any utility classes discovered under app/. This is what makes Tailwind classes you write in custom tools / ejected components / custom fields actually exist. Zero config to start:
# Gemfile
gem "tailwindcss-ruby"
Add custom Avo styles under app/assets/stylesheets/avo/ (they're built into the same stylesheet):
Run it with a watcher during dev (bin/rails avo:tailwindcss:watch in Procfile.dev). Extra scan roots via config.tailwindcss_content_sources (defaults to Rails.root.join("app")). Opt out with config.tailwindcss_integration_enabled = false.
Two files Avo loads on every screen, served as-is (not run through the Tailwind build). Because avo-overrides.css loads after Avo's stylesheet, overriding Avo's CSS variables here re-skins the whole admin with no build step (that's the avo-branding-appearance territory). Eject to customize: rails g avo:eject --partial :avo_overrides_css (or :avo_overrides_js, or :asset_overrides for both).
Tailwind 4 migration
Avo's build emits Tailwind v4 syntax. If you have your own Tailwind pipeline, migrate it to v4 and apply the breaking changes (renamed utilities like rounded→rounded-sm, explicit border colors since the default is now currentColor) to every custom field, resource tool, custom tool, custom card, and ejected component. Fetch the migration doc before touching a project that has an existing Tailwind config.
Advanced: package as a plugin
Wrap any of the above into a Rails Engine so it's reusable across apps. Register everything from the avo_boot hook so it runs once on boot; avo_init runs on every request.
register(name, priority: 10) — add to the plugin list (lower priority runs earlier).
register_view_type(name, component:, icon:, active_icon:) — a new index view type. Pass component: as a string ("MyPlugin::ViewTypes::TimelineViewTypeComponent") to dodge boot load-order issues; the component inherits Avo::ViewTypes::BaseViewTypeComponent and must render paginator_component. Enabling it on a resource is avo-index-views (self.view_types / self.default_view_type).
register_field(method_name, klass) — ship a field type from a gem (the plugin-author side of avo-custom-fields).
register_menu_item(name, &block) — a custom menu DSL method for config.main_menu. Delegates to avo-menu; no-op when avo-menu isn't installed, so register unconditionally.
mount_engine(klass, at:) — mount the engine's routes inside Avo.
installed?(name) — adapt to what else is present.
Assets from library code go through Avo.asset_manager (not the app pipeline); Avo injects them but does not compile them — ship compiled builds, e.g. served from app/assets/builds via a Rack::Static middleware.
Gotchas
Custom tool vs. resource tool vs. new field. Standalone page → custom tool. Widget on a record's Show/Edit → resource tool. A new input type used across resources → avo-custom-fields (not this).
Ejected files are frozen copies — you own them forever. They stop receiving upstream updates; Avo bug/security fixes won't reach your copy. Prefer CSS-variable / Tailwind-layer overrides and --scope (so you don't replace a component everywhere) before ejecting whole components.
The no-mount_avo fallback route is NOT behind auth. A custom-tool route inside mount_avo inherits Avo's authentication. But if config/routes.rb has no mount_avo at all, the generator appends a standalone Avo::Engine.routes.draw block that is unprotected — secure it yourself.
Resource-tool custom inputs need all three steps. Missing self.extra_params (params not permitted) or a model that doesn't respond to the attribute → the value silently never persists. Nested keys not listed in extra_params are dropped too.
form is only present on New/Edit. Guard resource-tool partials with if form.present? or they blow up on Show.
Prefer components over hand-rolled divs.ui.panel / ui.card with padded: / title: give you Avo's spacing, headers, and dark mode; utility classes on bare <div>s drift out of sync and miss dark mode.
avo-overrides.js runs once, but Avo navigates with Turbo. One-shot DOM edits vanish on the next visit — use a Stimulus controller (re-connects each visit) or a turbo:load listener.
avo-overrides.css is served as-is — NOT compiled by Tailwind.@apply and utility classes won't be generated there; it's for raw CSS and CSS-variable overrides. Put Tailwind-using CSS under app/assets/stylesheets/avo/.
Tailwind integration silently stays off unless tailwindcss-ruby is present, and — if you pull Tailwind via tailwindcss-rails — that gem is >= 4.0 (on 3.x the integration is disabled even though tailwindcss-ruby is there). Symptom: your utility classes don't exist in the admin.
New JS/CSS not loading? You skipped the asset-pipeline entrypoint. Run avo:js:install (or wire avo.custom.js manually) — self.stimulus_controllers alone loads nothing.
Plugin registration must be inside ActiveSupport.on_load(:avo_boot), register_view_type's component: should be a string, and register_menu_item is a no-op without avo-menu.
Report
When done, tell the user:
Which surface you built (custom tool / resource tool / ejected view / plugin) and the exact files created or edited (absolute paths), including the resource(s) you registered a tool on.
Which components/patterns it uses (ui.panel/ui.card, a_button/a_link, pre-made toggle/disable, dependent-select controller, ejected + --scope).
For form inputs: that you added self.extra_params and whether the model already responds to those attributes (or the setters the user still needs to add).
Asset/Tailwind wiring done vs. left to the user: the avo:js:install entrypoint, tailwindcss-ruby (and tailwindcss-rails >= 4.0), the watcher process.
Any follow-ups they must do: secure a fallback (non-mount_avo) route, maintain ejected copies on upgrades, permit missing params, or compile/serve plugin assets.