Class: Seams::Generators::DesignGenerator
- Inherits:
-
Rails::Generators::Base
- Object
- Rails::Generators::Base
- Seams::Generators::DesignGenerator
- Includes:
- EjectAware, HostInjector
- Defined in:
- lib/generators/seams/design/design_generator.rb
Overview
Generates the canonical Design engine on top of the generic engine
scaffold — Phase 1 (this unit): the engine skeleton, the non-isolated
wiring that makes ui_* helpers + ui/ partials visible host-wide, the
Tailwind v4 token injection, the FormBuilder default, and the icon sprite
render.
The design engine is DELIBERATELY NOT isolate_namespaced (D4 in
proposals/design_system_engine.md). The whole value is that a component
renders anywhere in the host and in every other engine's views without
ceremony, which requires the partials and the helper to live in the host's
view paths and ActionController::Base. The base EngineGenerator produces
an isolated engine, so this generator overwrites lib/design/engine.rb with
the non-isolated form and removes the single-namespace leftovers the base
scaffold ships.
Naming (D1): the engine, CLI verb, folder and Ruby namespace are design
(Design::); the VIEW + HELPER surface is ui — partials at
app/views/ui/_ui_<name>.
Later sub-issues import this skeleton: #17 (tokens/theme), #18 (helpers + gallery + tests), #19 (FormBuilder + form components), #20 (the design:component generator), and the Phase 2/3 component + shell units.
Run with: bin/seams design (or bin/rails generate seams:design)
Like the admin generator, this is a long-but-flat orchestration class: each public method is one small, single-purpose generate step, so the length is inherent to the number of files the engine ships, not tangled logic. rubocop:disable-next Metrics/ClassLength
Constant Summary collapse
- ENGINE_NAME =
"design"
Constants included from EjectAware
EjectAware::EJECT_HEADER_PREFIX
Instance Method Summary collapse
-
#create_actions_components ⇒ Object
The Actions & status component set (Phase 2, GROUPKEY = actions).
-
#create_auto_wire ⇒ Object
The auto-wire registry: Design.component_names derives the public component list from the preview partials, and resets on reload so a new component appears without a server restart.
- #create_base_engine ⇒ Object
-
#create_component_generator ⇒ Object
The design:component generator (#20): ships INSIDE the generated engine at engines/design/lib/generators/design/component/, so a host can run
rails g design:component <name>(Rails auto-discovers it on the engine's lib path; no registration needed). -
#create_data_components ⇒ Object
The Data display component set (Phase 2, GROUPKEY = data).
-
#create_example_theme ⇒ Object
Ship the example "quire" theme (#27) into the host as a token overlay the host can opt into.
-
#create_form_builder ⇒ Object
The default form builder.
-
#create_form_components ⇒ Object
The form-input component set (#19): the building blocks Design::FormBuilder and hand-written forms render.
-
#create_guide ⇒ Object
The living gallery (dev/test only).
-
#create_helper ⇒ Object
The host-wide helper module.
-
#create_icon_partials ⇒ Object
The icon sprite + icon partials — the minimum view surface the skeleton needs so
render "ui/icon_sprite"(wired into the host layout below) and ui_icon resolve. -
#create_nav_components ⇒ Object
The Navigation component set (Phase 2, GROUPKEY = nav).
-
#create_overlays_components ⇒ Object
The Overlays component set (Phase 2, GROUPKEY = overlays).
-
#create_primitive_components ⇒ Object
The Primitives & icons component set (Phase 2, GROUPKEY = primitives).
- #create_runtime_spec ⇒ Object
-
#create_seed_components ⇒ Object
The seed component set — enough for the gallery + the contract/render tests to have something real to render.
-
#create_shell ⇒ Object
The opt-in app shell (#26), generated ONLY with --shell.
-
#overwrite_engine_entry_point ⇒ Object
The base EngineGenerator emits an ISOLATED engine.
- #overwrite_readme ⇒ Object
-
#remove_isolated_leftovers ⇒ Object
The base scaffold ships an isolated engine's ApplicationController and ApplicationRecord under app/controllers/design/ and app/models/design/.
- #report_summary ⇒ Object
-
#wire_into_host ⇒ Object
--- Host wiring ----------------------------------------------------------.
Methods included from EjectAware
#ejected?, #template_unless_ejected
Methods included from HostInjector
#host_inject_gem, #host_inject_include_in_application_controller, #host_inject_include_in_user, #host_inject_mount, #host_uninject_gem, #host_uninject_include, #host_uninject_mount, #routes_draw_anchor
Instance Method Details
#create_actions_components ⇒ Object
The Actions & status component set (Phase 2, GROUPKEY = actions). Ported faithfully from quire-saas's compositor (compositor_* -> ui_*, quire copy neutralised in the previews), each carrying its baked-in accessibility:
- banner a page-level role=region announcement with a tone variant;
- toast a role=status transient notification;
- note an inline annotation span.
Each ships with a companion preview, which is what makes it "public": the
auto-wire derives ui_
269 270 271 272 273 274 275 276 |
# File 'lib/generators/seams/design/design_generator.rb', line 269 def create_actions_components %w[banner toast note].each do |name| template_unless_ejected "app/views/ui/_#{name}.html.erb.tt", engine_path("app/views/ui/_#{name}.html.erb") template_unless_ejected "app/views/ui/previews/_#{name}.html.erb.tt", engine_path("app/views/ui/previews/_#{name}.html.erb") end end |
#create_auto_wire ⇒ Object
The auto-wire registry: Design.component_names derives the public component list from the preview partials, and resets on reload so a new component appears without a server restart. Ported from quire-saas's lib/compositor.rb (compositor -> design, compositor/previews -> ui/previews).
111 112 113 |
# File 'lib/generators/seams/design/design_generator.rb', line 111 def create_auto_wire template "lib/design/components.rb.tt", engine_path("lib/design/components.rb") end |
#create_base_engine ⇒ Object
59 60 61 62 63 64 65 66 67 68 69 |
# File 'lib/generators/seams/design/design_generator.rb', line 59 def create_base_engine # The base EngineGenerator raises if engines/design/ already exists. # Skip it on a re-run so a second `bin/seams design` is a no-op on the # engine and simply re-applies the (idempotent) host wiring below. if File.directory?(engine_path("")) say " exist engines/design (kept — re-applying host wiring only)", :blue return end EngineGenerator.start([ENGINE_NAME], destination_root: destination_root) end |
#create_component_generator ⇒ Object
The design:component generator (#20): ships INSIDE the generated engine
at engines/design/lib/generators/design/component/, so a host can run
rails g design:component <name> (Rails auto-discovers it on the engine's
lib path; no registration needed). Ported from quire-saas's Compositor.
copy_file (NOT template): the generator's own .tt templates are ERB run by
rails g design:component, so they must reach the engine VERBATIM. The
meta-generator's ERB would un-escape their <%% markers and nest the
<%= file_name %> placeholders inside a real tag — a parse error.
326 327 328 329 330 331 332 333 |
# File 'lib/generators/seams/design/design_generator.rb', line 326 def create_component_generator copy_file "lib/generators/design/component/component_generator.rb.tt", engine_path("lib/generators/design/component/component_generator.rb") copy_file "lib/generators/design/component/templates/component.html.erb.tt", engine_path("lib/generators/design/component/templates/component.html.erb.tt") copy_file "lib/generators/design/component/templates/preview.html.erb.tt", engine_path("lib/generators/design/component/templates/preview.html.erb.tt") end |
#create_data_components ⇒ Object
The Data display component set (Phase 2, GROUPKEY = data). Ported faithfully from quire-saas's compositor (compositor_* -> ui_*, quire copy neutralised in the previews), each carrying its baked-in accessibility:
- card a titled content surface;
- data_table a <table> with a caption + scoped headers;
- chapter_row a manuscript chapter list row;
- build_row an export/build status list row;
- counter a labelled numeric stat;
- meter a <meter>-backed progress indicator;
- kbd a <kbd> keyboard-shortcut glyph.
Each ships with a companion preview, which is what makes it "public": the
auto-wire derives ui_
293 294 295 296 297 298 299 300 |
# File 'lib/generators/seams/design/design_generator.rb', line 293 def create_data_components %w[card data_table chapter_row build_row counter meter kbd].each do |name| template_unless_ejected "app/views/ui/_#{name}.html.erb.tt", engine_path("app/views/ui/_#{name}.html.erb") template_unless_ejected "app/views/ui/previews/_#{name}.html.erb.tt", engine_path("app/views/ui/previews/_#{name}.html.erb") end end |
#create_example_theme ⇒ Object
Ship the example "quire" theme (#27) into the host as a token overlay the
host can opt into. It is NOT applied by default (the neutral theme owns
the default, per the proposal) — it sits alongside application.css as the
worked proof that retheming is a token override: add one
@import "themes/quire"; line and the whole app reskins. The theming
guide (doc/design-system/DESIGN_SYSTEM_THEMING.md) documents it. Eject-aware.
378 379 380 381 |
# File 'lib/generators/seams/design/design_generator.rb', line 378 def create_example_theme template_unless_ejected "app/assets/tailwind/themes/_quire.css", host_path("app/assets/tailwind/themes/_quire.css") end |
#create_form_builder ⇒ Object
The default form builder. Subclasses the standard Rails builder and only ADDS ui_* methods, so it is safe as the app-wide default. Ported from quire-saas's app/form_builders/compositor/form_builder.rb (compositor_* -> ui_*, the field partial path compositor/field -> ui/field). #19 fleshes out the textarea/select/submit helpers + the ui/field partial.
128 129 130 131 |
# File 'lib/generators/seams/design/design_generator.rb', line 128 def create_form_builder template "app/form_builders/design/form_builder.rb.tt", engine_path("app/form_builders/design/form_builder.rb") end |
#create_form_components ⇒ Object
The form-input component set (#19): the building blocks Design::FormBuilder and hand-written forms render. Ported faithfully from quire-saas's compositor (compositor_* -> ui_*, quire copy neutralised in the previews):
- field the label/input/hint/error wrapper with baked-in
aria-invalid + aria-describedby wiring (what
f.ui_text_field renders);
- checkbox an accessible labelled checkbox with an optional hint;
- radio a labelled radio (grouped by name in a fieldset);
- switch a role="switch" toggle;
- input_group a text input with an optional prefix/suffix affix.
Each ships with a companion preview, which is what makes it "public": the
auto-wire derives ui_
148 149 150 151 152 153 154 155 |
# File 'lib/generators/seams/design/design_generator.rb', line 148 def create_form_components %w[field checkbox radio switch input_group].each do |name| template_unless_ejected "app/views/ui/_#{name}.html.erb.tt", engine_path("app/views/ui/_#{name}.html.erb") template_unless_ejected "app/views/ui/previews/_#{name}.html.erb.tt", engine_path("app/views/ui/previews/_#{name}.html.erb") end end |
#create_guide ⇒ Object
The living gallery (dev/test only). The controller renders every
component from its preview so the docs cannot drift; the route is guarded
to Rails.env.local? both in the controller (404 in production) and at the
host routes (drawn inside an if Rails.env.local? block). Ported from
quire-saas's compositor guide. Eject-aware so a host can restyle the
gallery chrome.
308 309 310 311 312 313 314 315 |
# File 'lib/generators/seams/design/design_generator.rb', line 308 def create_guide template "app/controllers/design/guide_controller.rb.tt", engine_path("app/controllers/design/guide_controller.rb") template_unless_ejected "app/views/layouts/design/guide.html.erb.tt", engine_path("app/views/layouts/design/guide.html.erb") template_unless_ejected "app/views/design/guide/index.html.erb.tt", engine_path("app/views/design/guide/index.html.erb") end |
#create_helper ⇒ Object
The host-wide helper module. ui_icon is the one hand-written helper;
define_component_helpers! auto-derives ui_
118 119 120 121 |
# File 'lib/generators/seams/design/design_generator.rb', line 118 def create_helper template "app/helpers/design/ui_helper.rb.tt", engine_path("app/helpers/design/ui_helper.rb") end |
#create_icon_partials ⇒ Object
The icon sprite + icon partials — the minimum view surface the skeleton
needs so render "ui/icon_sprite" (wired into the host layout below) and
ui_icon resolve. #25 ships the full primitive + icon set; these two are
the load-bearing pair the host layout references on first boot.
161 162 163 164 165 166 |
# File 'lib/generators/seams/design/design_generator.rb', line 161 def create_icon_partials template_unless_ejected "app/views/ui/_icon.html.erb.tt", engine_path("app/views/ui/_icon.html.erb") template_unless_ejected "app/views/ui/_icon_sprite.html.erb.tt", engine_path("app/views/ui/_icon_sprite.html.erb") end |
#create_nav_components ⇒ Object
The Navigation component set (Phase 2, GROUPKEY = nav). Ported faithfully from quire-saas's compositor (compositor_* -> ui_*, quire copy neutralised in the previews), each carrying its baked-in navigation accessibility roles/aria:
- breadcrumb a nav[aria-label=Breadcrumb] trail with aria-current=page;
- pagination a nav[aria-label=Pagination] with per-page aria-current;
- menu a role=menu list of role=menuitem links/buttons;
- segmented a role=group of aria-pressed toggle buttons;
- stepper an ordered list with aria-current=step + done ticks;
- toolbar a role=toolbar of labelled icon/text buttons;
- outline a nav[aria-label=Outline] heading tree with aria-current.
Each ships with a companion preview, which is what makes it "public": the
auto-wire derives ui_
202 203 204 205 206 207 208 209 |
# File 'lib/generators/seams/design/design_generator.rb', line 202 def create_nav_components %w[breadcrumb pagination menu segmented stepper toolbar outline].each do |name| template_unless_ejected "app/views/ui/_#{name}.html.erb.tt", engine_path("app/views/ui/_#{name}.html.erb") template_unless_ejected "app/views/ui/previews/_#{name}.html.erb.tt", engine_path("app/views/ui/previews/_#{name}.html.erb") end end |
#create_overlays_components ⇒ Object
The Overlays component set (Phase 2, GROUPKEY = overlays). Ported faithfully from quire-saas's compositor (compositor_* -> ui_*, compositor-dialog controller -> ui-dialog, quire copy neutralised in the previews), each carrying its baked-in overlay accessibility:
- dialog a native <dialog aria-labelledby> with a labelled close
button, driven by a ui-dialog Stimulus controller the host
supplies (data-controller / data-action wiring baked in);
- drawer an <aside aria-label> side-panel landmark;
- popover a role=note annotation bubble;
- savestate a role=status live region (saved / saving / unsaved).
Each ships with a companion preview, which is what makes it "public": the
auto-wire derives ui_
226 227 228 229 230 231 232 233 |
# File 'lib/generators/seams/design/design_generator.rb', line 226 def %w[dialog drawer popover savestate].each do |name| template_unless_ejected "app/views/ui/_#{name}.html.erb.tt", engine_path("app/views/ui/_#{name}.html.erb") template_unless_ejected "app/views/ui/previews/_#{name}.html.erb.tt", engine_path("app/views/ui/previews/_#{name}.html.erb") end end |
#create_primitive_components ⇒ Object
The Primitives & icons component set (Phase 2, GROUPKEY = primitives). The icon + icon_sprite primitives ship from create_icon_partials above; this set adds the remaining low-level building blocks, ported faithfully from quire-saas's compositor (compositor_* -> ui_*, quire copy neutralised in the previews), each carrying its baked-in accessibility:
- panel a plain raised content surface (a content-block wrapper);
- diff a per-line add/del/ctx list whose +/- signs are aria-labelled
"added"/"removed" so the glyph alone is not load-bearing;
- empty an empty-state with a required title + content-block body.
Each ships with a companion preview, which is what makes it "public": the
auto-wire derives ui_
249 250 251 252 253 254 255 256 |
# File 'lib/generators/seams/design/design_generator.rb', line 249 def create_primitive_components %w[panel diff empty].each do |name| template_unless_ejected "app/views/ui/_#{name}.html.erb.tt", engine_path("app/views/ui/_#{name}.html.erb") template_unless_ejected "app/views/ui/previews/_#{name}.html.erb.tt", engine_path("app/views/ui/previews/_#{name}.html.erb") end end |
#create_runtime_spec ⇒ Object
335 336 337 338 339 340 341 342 343 344 |
# File 'lib/generators/seams/design/design_generator.rb', line 335 def create_runtime_spec template "spec/runtime/design_boot_spec.rb.tt", engine_path("spec/runtime/design_boot_spec.rb") template "spec/runtime/ui_components_spec.rb.tt", engine_path("spec/runtime/ui_components_spec.rb") template "spec/runtime/form_builder_spec.rb.tt", engine_path("spec/runtime/form_builder_spec.rb") template "spec/runtime/guide_spec.rb.tt", engine_path("spec/runtime/guide_spec.rb") end |
#create_seed_components ⇒ Object
The seed component set — enough for the gallery + the contract/render
tests to have something real to render. Ported faithfully from
quire-saas's compositor (compositor_* -> ui_*, quire copy neutralised):
_button (takes a content block + variant/size) and _tag (a required
label: strict local — the contract test relies on it being required).
Each ships with a companion preview, which is what makes it "public":
the auto-wire derives ui_
177 178 179 180 181 182 183 184 |
# File 'lib/generators/seams/design/design_generator.rb', line 177 def create_seed_components %w[button tag].each do |name| template_unless_ejected "app/views/ui/_#{name}.html.erb.tt", engine_path("app/views/ui/_#{name}.html.erb") template_unless_ejected "app/views/ui/previews/_#{name}.html.erb.tt", engine_path("app/views/ui/previews/_#{name}.html.erb") end end |
#create_shell ⇒ Object
The opt-in app shell (#26), generated ONLY with --shell. Without the flag none of these files appear and the host keeps its rails-new layout.
- app/views/layouts/application.html.erb — the HOST's default layout,
overwritten (force) with one built entirely from ui_* components
(header, nav, flash banners, footer). Eject-aware so a host that has
already customised it on a later run keeps its version.
- the starter signed-in dashboard controller + view, shipped INTO the
engine (Design::DashboardController subclasses the host's
ApplicationController), with an empty-state listing the engines the
host could add. The route + root are drawn in wire_into_host.
361 362 363 364 365 366 367 368 369 370 |
# File 'lib/generators/seams/design/design_generator.rb', line 361 def create_shell return unless shell? say " shell generating the opt-in app shell (--shell)", :green create_shell_layout template "app/controllers/design/dashboard_controller.rb.tt", engine_path("app/controllers/design/dashboard_controller.rb") template_unless_ejected "app/views/design/dashboard/index.html.erb.tt", engine_path("app/views/design/dashboard/index.html.erb") end |
#overwrite_engine_entry_point ⇒ Object
The base EngineGenerator emits an ISOLATED engine. The design engine is non-isolated by design (D4), so overwrite lib/design/engine.rb with the non-isolated form that auto-wires the helper into ActionController::Base. engine.rb stays framework-managed (NOT eject-aware), like every other canonical generator's engine.rb.
76 77 78 79 |
# File 'lib/generators/seams/design/design_generator.rb', line 76 def overwrite_engine_entry_point template "lib/engine.rb.tt", engine_path("lib/design/engine.rb"), force: true template "lib/design.rb.tt", engine_path("lib/design.rb"), force: true end |
#overwrite_readme ⇒ Object
346 347 348 |
# File 'lib/generators/seams/design/design_generator.rb', line 346 def overwrite_readme template "README.md.tt", engine_path("README.md"), force: true end |
#remove_isolated_leftovers ⇒ Object
The base scaffold ships an isolated engine's ApplicationController and ApplicationRecord under app/controllers/design/ and app/models/design/. A view-layer engine needs neither — its only controller is the dev-only guide (created below) and it has no models — so remove the leftovers.
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
# File 'lib/generators/seams/design/design_generator.rb', line 85 def remove_isolated_leftovers # config/routes.rb is KEPT (an empty `Design::Engine.routes.draw do end`) # so the engine stays mountable in the dummy app + host; the dev-only # guide route (#18) is drawn into it later. %w[ app/controllers/design/application_controller.rb app/models/design/application_record.rb spec/design_spec.rb ].each do |relative| full = engine_path(relative) next unless File.exist?(full) FileUtils.rm(full) say " remove #{relative} (isolated-engine leftover)", :red end %w[app/controllers/design app/models/design].each do |relative| full = engine_path(relative) Dir.rmdir(full) if File.directory?(full) && Dir.empty?(full) end end |
#report_summary ⇒ Object
397 398 399 |
# File 'lib/generators/seams/design/design_generator.rb', line 397 def report_summary say report_summary_text, :green end |
#wire_into_host ⇒ Object
--- Host wiring ----------------------------------------------------------
385 386 387 388 389 390 391 392 393 394 395 |
# File 'lib/generators/seams/design/design_generator.rb', line 385 def wire_into_host # Tailwind v4 is a hard dependency (D2): the @theme token layer the # engine ships is Tailwind-native. Inject the gem and write the token # block into the host's application.css. host_inject_gem("tailwindcss-rails", "~> 4.0") inject_theme_into_host_css set_host_default_form_builder render_sprite_in_host_layout draw_guide_route_in_host draw_dashboard_route_in_host if shell? end |