Generators
terrazzo:install
Sets up Terrazzo in your Rails app.
rails g terrazzo:installOptions
| Option | Default | Description |
|---|---|---|
--namespace | admin | Admin namespace (controller prefix, route scope, JS directory) |
--bundler | vite | JavaScript bundler. vite emits vite_client_tag and vite_javascript_tag for the explicit admin JSX entrypoint; esbuild emits javascript_include_tag and a root app/javascript/admin.js entrypoint for Rails' default esbuild glob. Page mappings are explicit and can be extended through custom_page_mapping.js. |
# Vite (default)
rails g terrazzo:install
# esbuild
rails g terrazzo:install --bundler=esbuild
# Custom namespace
rails g terrazzo:install --namespace=backstageFor older scripts, rails g terrazzo:install backstage is also accepted as a namespace alias. Prefer --namespace; do not combine both forms with different values.
Creates:
- Admin Superglue infrastructure (entry point, page mapping, visit handler, flash slice, layouts)
- A Vite entrypoint shim under the app's configured Vite entrypoints directory when
--bundler=vite(default:app/frontend/entrypoints/admin/application.jsx) - Vite
watchAdditionalPathsentries for generated admin JS and view files that live outside Vite'ssourceCodeDir - Vite local-development wiring so
Procfile.devrunsbin/vite devand a stock Rails-onlybin/devstartsProcfile.dev app/javascript/admin/generated_page_mapping.jsfor resource-specific view mappings generated by Terrazzoapp/javascript/admin/custom_page_mapping.jsfor manual page mappings you ownapp/assets/stylesheets/admin.csswith Tailwind sources for package defaults and local admin filescomponents.jsonwith shadcn aliases for the admin component and UI primitive paths, unless the app already has onejsconfig.jsonwith a root@/*path alias for shadcn-style tooling, unless the app already hasjsconfig.jsonortsconfig.json- Admin application controller
- Admin routes namespace
- Dashboards for all existing
ApplicationRecordmodels - App-level component, field, and UI barrels that fall back to the
terrazzopackage
When --bundler=vite, the installer requires vite_rails because the generated admin layout uses Vite Rails asset helpers. Add gem "vite_rails" and run bundle exec vite install first so config/vite.json, vite, and vite-plugin-ruby are present, or pass --bundler=esbuild. For Vite installs, the generator also ensures Procfile.dev includes vite: bin/vite dev. If bin/dev is the stock Rails-only server script, it is replaced with a Foreman runner. Custom bin/dev scripts are preserved and reported so you can wire Vite into your own dev process. The generator checks package.json for the required Terrazzo frontend packages and prints the package-manager command to install anything missing or pinned below Terrazzo's supported version floors. This includes Tailwind CSS 4 or newer because the generated admin stylesheet uses Tailwind 4 syntax. It also adds a build:admin:css package script when @tailwindcss/cli 4 or newer is installed and no Tailwind build script or Tailwind 4-compatible tailwindcss-rails setup compiles Terrazzo's Rails-linked admin stylesheet. For Vite installs, it adds a package build script when one is missing; if Terrazzo created build:admin:css, that CSS command is included in build. If Tailwind package tooling is missing or too old, it warns with the matching install command. The @tailwindcss/vite plugin alone is not treated as sufficient because the generated layout links app/assets/stylesheets/admin.css as a Rails stylesheet asset. When components.json does not exist, the install generator creates one that points shadcn CLI output at app/views/admin/components and app/views/admin/components/ui (or the matching custom namespace). Existing components.json files are left untouched. When neither jsconfig.json nor tsconfig.json exists, the installer also creates a small jsconfig.json so editor tooling and shadcn-style generators can resolve root @/* imports. Existing JS/TS configs are left untouched. The installer also verifies that at least one concrete ApplicationRecord model exists and that those model tables exist before generating dashboards. Run bin/rails db:prepare first if the generator reports missing tables. Resource-specific view generators update generated_page_mapping.js; add hand-written custom pages to custom_page_mapping.js.
terrazzo:dashboard
Generates a dashboard for a specific model.
rails g terrazzo:dashboard Product
rails g terrazzo:dashboard Blog::PostThe model must already exist; the generator inspects the model class to infer fields, associations, enums, rich text, and attachments.
Options
| Option | Default | Description |
|---|---|---|
--namespace | admin | Admin namespace |
--bundler | vite | JavaScript bundler (vite or esbuild). Resource-specific view mappings are handled by generated_page_mapping.js when needed. |
Creates:
app/dashboards/product_dashboard.rbapp/controllers/admin/products_controller.rb
For namespaced models, terrazzo:dashboard Blog::Post writes app/dashboards/blog/post_dashboard.rb and app/controllers/admin/blog/posts_controller.rb with explicit Blog::PostDashboard and Admin::Blog::PostsController module nesting. Deeper namespaces follow the same convention, so Store::Catalog::Product writes app/dashboards/store/catalog/product_dashboard.rb and app/controllers/admin/store/catalog/products_controller.rb.
The generator inspects your model's columns and associations:
- Primary key columns →
Field::String - String columns →
Field::String - Integer/float columns →
Field::Number - Boolean columns →
Field::Boolean - Date columns →
Field::Date - DateTime columns →
Field::DateTime - Text columns →
Field::Text has_rich_textattributes →Field::RichText- Enums →
Field::Selectwith collection belongs_toassociations →Field::BelongsTobelongs_topolymorphic associations →Field::Polymorphichas_manyassociations →Field::HasManyhas_oneassociations →Field::HasOnehas_one_attachedActive Storage attachments →Field::Asset
FORM_ATTRIBUTES excludes id, created_at, and updated_at. COLLECTION_ATTRIBUTES is limited to 4 attributes.
terrazzo:field
Generates a custom field type and app-owned React components.
rails g terrazzo:field RatingCreates:
app/fields/terrazzo/field/rating.rbapp/views/admin/fields/rating/IndexField.jsxapp/views/admin/fields/rating/ShowField.jsxapp/views/admin/fields/rating/FormField.jsx
The generator creates or updates the app-level field and UI barrels, then registers the custom field components in app/views/admin/fields/index.js so package pages and ejected pages can render Field::Rating.
For custom namespaces, pass the same namespace you used at install time:
rails g terrazzo:field Rating --namespace=backstage| Option | Default | Description |
|---|---|---|
--namespace | admin | Admin namespace whose field barrel should register the custom field components |
terrazzo:views
Regenerates the shared page stubs, app-level barrels, and navigation props.
rails g terrazzo:viewsUse this after upgrading the gem to refresh the default wiring. This will overwrite local edits to those generated stubs, barrels, and navigation props. Eject individual files when you want app-owned React source.
terrazzo:views:index, terrazzo:views:show, terrazzo:views:new, terrazzo:views:edit
Ejects a view for customization. Without a resource argument, ejects the shared JSX page component. With a resource argument, ejects a complete resource-specific view: the page JSX, its associated partial (_collection.jsx or _form.jsx), and a .json.props that extends the base serialization.
If the app-level field, component, or UI barrels are missing, these generators create the base package re-exports before writing the page files. When a shared page target still contains Terrazzo's generated package stub, the generator replaces it without a conflict prompt. Customized shared pages still use Rails' normal overwrite prompt.
# Eject the shared JSX page component
rails g terrazzo:views:show
# Eject a resource-specific view
rails g terrazzo:views:show User
rails g terrazzo:views:index Order
rails g terrazzo:views:edit Product
rails g terrazzo:views:new BlogPost
rails g terrazzo:views:index Catalog::Product
rails g terrazzo:views:index catalog/productResource arguments accept either Ruby constant style (Catalog::Product) or Rails path style (catalog/product).
When ejecting for a resource, the generator creates a complete set of files:
terrazzo:views:index User→index.jsx+_collection.jsx+index.json.propsterrazzo:views:edit User→edit.jsx+_form.jsx+edit.json.propsterrazzo:views:new User→new.jsx+_form.jsx+new.json.propsterrazzo:views:show User→show.jsx+show.json.props
Because edit and new share the same _form.jsx partial via a relative import, ejecting one without the other means the non-ejected view continues using the built-in form. If the counterpart is still Terrazzo's generated package stub, form-page ejection replaces it automatically so both pages use the same app-owned form. For missing resource-specific counterparts, the edit and new generators prompt unless you pass --with-counterpart or --no-with-counterpart.
The ejected .json.props calls the gem's base partial and leaves room for custom props:
# app/views/admin/users/show.json.props
json.partial! partial: "terrazzo/application/show_base"
# Add custom props below:
# json.customProp @resource.some_methodResource-specific view generators register the page in app/javascript/admin/generated_page_mapping.js and make sure page_to_page_mapping.js merges that manifest. If the main mapping file has been customized so heavily that Terrazzo cannot find a pages object to merge into, the generator fails instead of creating an unreachable page. Either restore the generated mapping shape or wire generatedPageMapping into your custom mapping file yourself.
Options
| Option | Default | Description |
|---|---|---|
--namespace | admin | Admin namespace |
--with-counterpart / --no-with-counterpart | prompt | For views:new and views:edit, choose whether to also eject the counterpart page that shares _form.jsx |
terrazzo:eject
Ejects supported app-owned starter files from Terrazzo.
rails g terrazzo:eject pages/index
rails g terrazzo:eject pages/edit
rails g terrazzo:eject fields/string
rails g terrazzo:eject fields/asset
rails g terrazzo:eject fields/number # also customizes Field::Money rendering
rails g terrazzo:eject components/Layout
rails g terrazzo:eject components/ResourceTable
rails g terrazzo:eject ui/button
rails g terrazzo:eject navigationFor custom namespaces, pass the same namespace you used at install time:
rails g terrazzo:eject pages/index --namespace=backstage| Option | Default | Description |
|---|---|---|
--namespace | admin | Admin namespace whose app-owned files should receive the ejected source |
Unsupported targets fail the generator instead of silently doing nothing, so typos are safe to catch in scripts and CI. When a target path still contains Terrazzo's generated package stub, ejection replaces it without a conflict prompt. If the file has already become app-owned/customized, Rails' normal overwrite prompt still applies. Dependency files copied only to satisfy the requested target's imports are non-destructive: Terrazzo skips them when they already exist and leaves their barrel exports alone. Run terrazzo:eject for that dependency directly when you want Rails' overwrite prompt and registration for it.
Ejected files import through app-level barrels:
app/views/admin/fields/index.jskeepsexport * from "terrazzo/fields"and registers local field overrides.app/views/admin/components/index.jskeepsexport * from "terrazzo/components"and registers local component overrides. Ejectingcomponents/Layoutalso registers it withsetLayout.app/views/admin/components/ui/index.jskeepsexport * from "terrazzo/ui"and exports local UI primitives for app-owned/ejected files.
If one of those barrels is missing, terrazzo:eject creates the base package re-export before writing the ejected file. This keeps standalone ejection commands buildable, while installed apps continue using the barrels created by terrazzo:views.
Page ejection copies required partials with the page when they are missing. For example, pages/index also copies _collection.jsx, and pages/edit or pages/new also copies _form.jsx. If you have already customized one of those shared partials, later page ejections preserve it. If the matching form page is still a generated package stub, ejecting pages/edit or pages/new also replaces that counterpart so both pages use the shared app-owned _form.jsx.
UI ejection copies required local primitive dependencies with the requested file when those dependencies are missing. For example, ui/pagination also copies ui/button, and ui/sidebar also copies the primitives it imports locally. Ejected UI primitives are consumed by app-owned pages, fields, and components that import from ../components/ui; packaged default pages continue using the package defaults until you eject or override the relevant page, field, or component.
The default index already supports search, filter facets, sortable rows, row actions, bulk actions, toolbar actions, page header actions, CSV exports, empty states, header/row/cell metadata, and a pagination footer with page and total-count context. Prefer dashboard hooks such as attribute_label, attribute_hint, collection_header_options, collection_row_options, collection_cell_options, collection_item_actions, collection_bulk_actions, collection_toolbar_actions, layout_actions, csv_attributes, empty_collection_message, and the NAVIGATION_* constants before ejecting full pages or navigation.
The older terrazzo:views:field, terrazzo:views:layout, and terrazzo:views:navigation commands remain as compatibility aliases for the ejection workflow. Prefer terrazzo:eject for new customization.
terrazzo:routes
Generates the admin namespace routes.
rails g terrazzo:routesInserts a namespace :admin block with resource routes and a root route into your config/routes.rb. The generator fails if it cannot find any concrete ApplicationRecord models, because there would be no valid resource controller for the admin root. Namespaced models generate matching nested route namespaces, so Store::Catalog::Product routes to admin/store/catalog/products instead of an invalid combined namespace. If the namespace already exists, it prints the cleanly indented routes to copy into that block instead of rewriting your existing route file.