seams 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +212 -12
- data/README.md +235 -82
- data/lib/generators/seams/accounts/accounts_generator.rb +31 -3
- data/lib/generators/seams/accounts/templates/app/controllers/memberships_controller.rb.tt +100 -0
- data/lib/generators/seams/accounts/templates/app/views/memberships/index.html.erb.tt +52 -0
- data/lib/generators/seams/accounts/templates/config/routes.rb.tt +10 -4
- data/lib/generators/seams/accounts/templates/lib/concerns/authorization.rb.tt +35 -2
- data/lib/generators/seams/accounts/templates/lib/engine.rb.tt +13 -0
- data/lib/generators/seams/accounts/templates/spec/runtime/authorization_spec.rb.tt +102 -0
- data/lib/generators/seams/accounts/templates/spec/runtime/memberships_flow_spec.rb.tt +145 -0
- data/lib/generators/seams/admin/admin_generator.rb +51 -4
- data/lib/generators/seams/admin/templates/README.md.tt +33 -3
- data/lib/generators/seams/admin/templates/app/controllers/admin/application_controller.rb.tt +156 -8
- data/lib/generators/seams/admin/templates/app/dashboards/admin/account_dashboard.rb.tt +9 -1
- data/lib/generators/seams/admin/templates/app/dashboards/admin/accounts_membership_dashboard.rb.tt +9 -1
- data/lib/generators/seams/admin/templates/app/dashboards/admin/identity_dashboard.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/invitation_dashboard.rb.tt +7 -1
- data/lib/generators/seams/admin/templates/app/dashboards/admin/invoice_dashboard.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/lifetime_pass_dashboard.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/notification_dashboard.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/notification_preference_dashboard.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/plan_dashboard.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/subscription_dashboard.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/dashboards/admin/team_dashboard.rb.tt +12 -2
- data/lib/generators/seams/admin/templates/app/dashboards/admin/teams_membership_dashboard.rb.tt +7 -1
- data/lib/generators/seams/admin/templates/app/fields/admin/fields/belongs_to.rb.tt +12 -0
- data/lib/generators/seams/admin/templates/app/fields/admin/fields/dashboard_option.rb.tt +22 -0
- data/lib/generators/seams/admin/templates/app/fields/admin/fields/has_many.rb.tt +12 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/account_policy.rb.tt +4 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/accounts_membership_policy.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/application_policy.rb.tt +51 -10
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/identity_policy.rb.tt +4 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/invitation_policy.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/invoice_policy.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/lifetime_pass_policy.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/notification_policy.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/notification_preference_policy.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/plan_policy.rb.tt +7 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/subscription_policy.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/team_policy.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/policies/admin/tenant/teams_membership_policy.rb.tt +6 -0
- data/lib/generators/seams/admin/templates/app/views/seams/admin/application/_index_header.html.erb.tt +57 -0
- data/lib/generators/seams/admin/templates/app/views/seams/admin/application/_navigation.html.erb.tt +14 -0
- data/lib/generators/seams/admin/templates/config/routes.rb.tt +24 -14
- data/lib/generators/seams/admin/templates/lib/concerns/authenticator.rb.tt +3 -0
- data/lib/generators/seams/admin/templates/lib/configuration.rb.tt +3 -1
- data/lib/generators/seams/admin/templates/spec/runtime/admin_boot_spec.rb.tt +4 -2
- data/lib/generators/seams/auth/add_oauth_provider/add_oauth_provider_generator.rb +1 -2
- data/lib/generators/seams/auth/auth_generator.rb +1 -2
- data/lib/generators/seams/auth/templates/app/controllers/oauth/callbacks_controller.rb.tt +3 -0
- data/lib/generators/seams/auth/templates/app/controllers/password_resets_controller.rb.tt +4 -0
- data/lib/generators/seams/auth/templates/app/controllers/registrations_controller.rb.tt +5 -0
- data/lib/generators/seams/auth/templates/app/controllers/sessions_controller.rb.tt +5 -0
- data/lib/generators/seams/auth/templates/db/migrate/create_auth_oauth_providers.rb.tt +5 -1
- data/lib/generators/seams/auth/templates/lib/concerns/authentication.rb.tt +12 -1
- data/lib/generators/seams/auth/templates/lib/engine.rb.tt +27 -0
- data/lib/generators/seams/billing/billing_generator.rb +4 -3
- data/lib/generators/seams/billing/templates/README.md.tt +26 -0
- data/lib/generators/seams/billing/templates/app/controllers/invoices_controller.rb.tt +2 -0
- data/lib/generators/seams/billing/templates/app/controllers/subscriptions_controller.rb.tt +2 -0
- data/lib/generators/seams/billing/templates/app/services/invoices/sync_service.rb.tt +8 -7
- data/lib/generators/seams/billing/templates/app/services/stripe_service.rb.tt +9 -7
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_handler_base.rb.tt +18 -3
- data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_handler_base.rb.tt +7 -21
- data/lib/generators/seams/billing/templates/lib/engine.rb.tt +15 -0
- data/lib/generators/seams/billing/templates/lib/gateways/stripe.rb.tt +10 -10
- data/lib/generators/seams/billing/templates/lib/stripe/client.rb.tt +40 -14
- data/lib/generators/seams/billing/templates/lib/stripe/payload.rb.tt +90 -0
- data/lib/generators/seams/billing/templates/lib/stripe/webhook_signature.rb.tt +11 -1
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/charge_refunded.json.tt +1 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/checkout_session_completed.json.tt +1 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_created.json.tt +15 -4
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_deleted.json.tt +7 -1
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_trial_will_end.json.tt +18 -1
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_updated.json.tt +26 -5
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_created.json.tt +16 -2
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_finalized.json.tt +16 -2
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_paid.json.tt +17 -2
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_payment_failed.json.tt +16 -2
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_voided.json.tt +16 -2
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/payment_intent_payment_failed.json.tt +1 -0
- data/lib/generators/seams/billing/templates/spec/fixtures/stripe/payment_intent_succeeded.json.tt +1 -0
- data/lib/generators/seams/billing/templates/spec/gateways/stripe_spec.rb.tt +47 -1
- data/lib/generators/seams/billing/templates/spec/runtime/webhook_handlers_spec.rb.tt +39 -1
- data/lib/generators/seams/design/design_generator.rb +647 -0
- data/lib/generators/seams/design/templates/README.md.tt +129 -0
- data/lib/generators/seams/design/templates/app/assets/tailwind/_tokens.css +322 -0
- data/lib/generators/seams/design/templates/app/assets/tailwind/themes/_quire.css +42 -0
- data/lib/generators/seams/design/templates/app/controllers/design/dashboard_controller.rb.tt +30 -0
- data/lib/generators/seams/design/templates/app/controllers/design/guide_controller.rb.tt +28 -0
- data/lib/generators/seams/design/templates/app/form_builders/design/form_builder.rb.tt +90 -0
- data/lib/generators/seams/design/templates/app/helpers/design/ui_helper.rb.tt +37 -0
- data/lib/generators/seams/design/templates/app/views/design/dashboard/index.html.erb.tt +38 -0
- data/lib/generators/seams/design/templates/app/views/design/guide/index.html.erb.tt +19 -0
- data/lib/generators/seams/design/templates/app/views/layouts/application.html.erb.tt +61 -0
- data/lib/generators/seams/design/templates/app/views/layouts/design/guide.html.erb.tt +21 -0
- data/lib/generators/seams/design/templates/app/views/ui/_banner.html.erb.tt +10 -0
- data/lib/generators/seams/design/templates/app/views/ui/_breadcrumb.html.erb.tt +8 -0
- data/lib/generators/seams/design/templates/app/views/ui/_build_row.html.erb.tt +14 -0
- data/lib/generators/seams/design/templates/app/views/ui/_button.html.erb.tt +4 -0
- data/lib/generators/seams/design/templates/app/views/ui/_card.html.erb.tt +2 -0
- data/lib/generators/seams/design/templates/app/views/ui/_chapter_row.html.erb.tt +11 -0
- data/lib/generators/seams/design/templates/app/views/ui/_checkbox.html.erb.tt +12 -0
- data/lib/generators/seams/design/templates/app/views/ui/_counter.html.erb.tt +2 -0
- data/lib/generators/seams/design/templates/app/views/ui/_data_table.html.erb.tt +25 -0
- data/lib/generators/seams/design/templates/app/views/ui/_dialog.html.erb.tt +14 -0
- data/lib/generators/seams/design/templates/app/views/ui/_diff.html.erb.tt +12 -0
- data/lib/generators/seams/design/templates/app/views/ui/_drawer.html.erb.tt +7 -0
- data/lib/generators/seams/design/templates/app/views/ui/_empty.html.erb.tt +5 -0
- data/lib/generators/seams/design/templates/app/views/ui/_field.html.erb.tt +14 -0
- data/lib/generators/seams/design/templates/app/views/ui/_icon.html.erb.tt +2 -0
- data/lib/generators/seams/design/templates/app/views/ui/_icon_sprite.html.erb.tt +22 -0
- data/lib/generators/seams/design/templates/app/views/ui/_input_group.html.erb.tt +11 -0
- data/lib/generators/seams/design/templates/app/views/ui/_kbd.html.erb.tt +4 -0
- data/lib/generators/seams/design/templates/app/views/ui/_menu.html.erb.tt +18 -0
- data/lib/generators/seams/design/templates/app/views/ui/_meter.html.erb.tt +15 -0
- data/lib/generators/seams/design/templates/app/views/ui/_note.html.erb.tt +2 -0
- data/lib/generators/seams/design/templates/app/views/ui/_outline.html.erb.tt +9 -0
- data/lib/generators/seams/design/templates/app/views/ui/_pagination.html.erb.tt +12 -0
- data/lib/generators/seams/design/templates/app/views/ui/_panel.html.erb.tt +2 -0
- data/lib/generators/seams/design/templates/app/views/ui/_popover.html.erb.tt +2 -0
- data/lib/generators/seams/design/templates/app/views/ui/_radio.html.erb.tt +5 -0
- data/lib/generators/seams/design/templates/app/views/ui/_savestate.html.erb.tt +5 -0
- data/lib/generators/seams/design/templates/app/views/ui/_segmented.html.erb.tt +7 -0
- data/lib/generators/seams/design/templates/app/views/ui/_stepper.html.erb.tt +12 -0
- data/lib/generators/seams/design/templates/app/views/ui/_switch.html.erb.tt +6 -0
- data/lib/generators/seams/design/templates/app/views/ui/_tag.html.erb.tt +2 -0
- data/lib/generators/seams/design/templates/app/views/ui/_toast.html.erb.tt +12 -0
- data/lib/generators/seams/design/templates/app/views/ui/_toolbar.html.erb.tt +15 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_banner.html.erb.tt +8 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_breadcrumb.html.erb.tt +5 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_build_row.html.erb.tt +6 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_button.html.erb.tt +9 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_card.html.erb.tt +12 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_chapter_row.html.erb.tt +6 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_checkbox.html.erb.tt +6 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_counter.html.erb.tt +6 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_data_table.html.erb.tt +14 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_dialog.html.erb.tt +8 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_diff.html.erb.tt +10 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_drawer.html.erb.tt +5 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_empty.html.erb.tt +5 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_field.html.erb.tt +5 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_input_group.html.erb.tt +5 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_kbd.html.erb.tt +5 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_menu.html.erb.tt +9 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_meter.html.erb.tt +6 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_note.html.erb.tt +2 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_outline.html.erb.tt +8 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_pagination.html.erb.tt +5 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_panel.html.erb.tt +5 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_popover.html.erb.tt +2 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_radio.html.erb.tt +6 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_savestate.html.erb.tt +4 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_segmented.html.erb.tt +15 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_stepper.html.erb.tt +6 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_switch.html.erb.tt +5 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_tag.html.erb.tt +8 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_toast.html.erb.tt +6 -0
- data/lib/generators/seams/design/templates/app/views/ui/previews/_toolbar.html.erb.tt +11 -0
- data/lib/generators/seams/design/templates/lib/design/components.rb.tt +22 -0
- data/lib/generators/seams/design/templates/lib/design.rb.tt +18 -0
- data/lib/generators/seams/design/templates/lib/engine.rb.tt +38 -0
- data/lib/generators/seams/design/templates/lib/generators/design/component/component_generator.rb.tt +52 -0
- data/lib/generators/seams/design/templates/lib/generators/design/component/templates/component.html.erb.tt +5 -0
- data/lib/generators/seams/design/templates/lib/generators/design/component/templates/preview.html.erb.tt +3 -0
- data/lib/generators/seams/design/templates/spec/runtime/design_boot_spec.rb.tt +79 -0
- data/lib/generators/seams/design/templates/spec/runtime/form_builder_spec.rb.tt +97 -0
- data/lib/generators/seams/design/templates/spec/runtime/guide_spec.rb.tt +40 -0
- data/lib/generators/seams/design/templates/spec/runtime/ui_components_spec.rb.tt +70 -0
- data/lib/generators/seams/engine/engine_generator.rb +6 -0
- data/lib/generators/seams/engine/templates/Gemfile.tt +1 -1
- data/lib/generators/seams/engine/templates/app/application_controller.rb.tt +6 -0
- data/lib/generators/seams/engine/templates/rubocop.yml.tt +9 -0
- data/lib/generators/seams/install/install_generator.rb +72 -6
- data/lib/generators/seams/install/templates/Dockerfile.tt +2 -2
- data/lib/generators/seams/install/templates/bin_seams.tt +7 -4
- data/lib/generators/seams/install/templates/ci.yml.tt +18 -8
- data/lib/generators/seams/install/templates/deploy.yml.tt +4 -2
- data/lib/generators/seams/install/templates/doc/ARCHITECTURE.md.tt +1 -1
- data/lib/generators/seams/install/templates/docker-entrypoint.tt +4 -2
- data/lib/generators/seams/install/templates/herb.yml.tt +10 -0
- data/lib/generators/seams/install/templates/lefthook.yml.tt +25 -0
- data/lib/generators/seams/install/templates/ruby-version.tt +1 -1
- data/lib/generators/seams/install/templates/seams.rake.tt +13 -1
- data/lib/generators/seams/install/templates/strong_migrations.rb.tt +26 -0
- data/lib/generators/seams/notifications/notifications_generator.rb +3 -2
- data/lib/generators/seams/notifications/templates/app/controllers/preferences_controller.rb.tt +5 -13
- data/lib/generators/seams/notifications/templates/lib/engine.rb.tt +12 -0
- data/lib/generators/seams/notifications/templates/lib/notifications.rb.tt +1 -0
- data/lib/generators/seams/notifications/templates/lib/preferences.rb.tt +36 -0
- data/lib/generators/seams/permissions/permissions_generator.rb +97 -0
- data/lib/generators/seams/permissions/templates/config/initializers/seams_permissions.rb.tt +35 -0
- data/lib/generators/seams/teams/templates/app/controllers/invitations_controller.rb.tt +27 -11
- data/lib/generators/seams/teams/templates/app/controllers/teams_controller.rb.tt +5 -13
- data/lib/generators/seams/teams/templates/lib/concerns/authorization.rb.tt +2 -4
- data/lib/generators/seams/teams/templates/lib/engine.rb.tt +13 -0
- data/lib/seams/cli/list.rb +15 -0
- data/lib/seams/cli/quality.rb +46 -5
- data/lib/seams/cli/resolve.rb +9 -5
- data/lib/seams/cli/test_changed.rb +5 -1
- data/lib/seams/cli.rb +20 -0
- data/lib/seams/configuration.rb +24 -1
- data/lib/seams/cops/no_cross_engine_dependency.rb +39 -7
- data/lib/seams/cops/no_cross_engine_model_access.rb +79 -1
- data/lib/seams/event_registry.rb +19 -0
- data/lib/seams/events/adapter.rb +9 -0
- data/lib/seams/events/adapters/active_support.rb +4 -0
- data/lib/seams/events/publisher.rb +29 -0
- data/lib/seams/events.rb +1 -0
- data/lib/seams/generators/dummy_app_writer.rb +21 -0
- data/lib/seams/generators/follow_up_generator.rb +1 -2
- data/lib/seams/generators/host_injector.rb +9 -2
- data/lib/seams/generators/splicer.rb +2 -2
- data/lib/seams/observability/adapter.rb +22 -0
- data/lib/seams/observability.rb +6 -0
- data/lib/seams/permission_registry.rb +66 -0
- data/lib/seams/permissions.rb +120 -0
- data/lib/seams/version.rb +1 -1
- data/lib/seams.rb +7 -0
- metadata +107 -3
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "rails/generators"
|
|
5
|
+
require "seams"
|
|
6
|
+
require "generators/seams/engine/engine_generator"
|
|
7
|
+
require "seams/generators/host_injector"
|
|
8
|
+
require "seams/generators/eject_aware"
|
|
9
|
+
|
|
10
|
+
module Seams
|
|
11
|
+
module Generators
|
|
12
|
+
# Generates the canonical Design engine on top of the generic engine
|
|
13
|
+
# scaffold — Phase 1 (this unit): the engine skeleton, the non-isolated
|
|
14
|
+
# wiring that makes `ui_*` helpers + `ui/` partials visible host-wide, the
|
|
15
|
+
# Tailwind v4 token injection, the FormBuilder default, and the icon sprite
|
|
16
|
+
# render.
|
|
17
|
+
#
|
|
18
|
+
# The design engine is DELIBERATELY NOT `isolate_namespace`d (D4 in
|
|
19
|
+
# proposals/design_system_engine.md). The whole value is that a component
|
|
20
|
+
# renders anywhere in the host and in every other engine's views without
|
|
21
|
+
# ceremony, which requires the partials and the helper to live in the host's
|
|
22
|
+
# view paths and `ActionController::Base`. The base EngineGenerator produces
|
|
23
|
+
# an isolated engine, so this generator overwrites lib/design/engine.rb with
|
|
24
|
+
# the non-isolated form and removes the single-namespace leftovers the base
|
|
25
|
+
# scaffold ships.
|
|
26
|
+
#
|
|
27
|
+
# Naming (D1): the engine, CLI verb, folder and Ruby namespace are `design`
|
|
28
|
+
# (`Design::`); the VIEW + HELPER surface is `ui` — partials at
|
|
29
|
+
# app/views/ui/_<name>.html.erb, previews at app/views/ui/previews/, and
|
|
30
|
+
# auto-derived helpers named `ui_<name>`.
|
|
31
|
+
#
|
|
32
|
+
# Later sub-issues import this skeleton: #17 (tokens/theme), #18 (helpers +
|
|
33
|
+
# gallery + tests), #19 (FormBuilder + form components), #20 (the
|
|
34
|
+
# design:component generator), and the Phase 2/3 component + shell units.
|
|
35
|
+
#
|
|
36
|
+
# Run with: bin/seams design (or bin/rails generate seams:design)
|
|
37
|
+
#
|
|
38
|
+
# Like the admin generator, this is a long-but-flat orchestration class: each
|
|
39
|
+
# public method is one small, single-purpose generate step, so the length is
|
|
40
|
+
# inherent to the number of files the engine ships, not tangled logic.
|
|
41
|
+
# rubocop:disable-next Metrics/ClassLength
|
|
42
|
+
class DesignGenerator < Rails::Generators::Base
|
|
43
|
+
include Seams::Generators::HostInjector
|
|
44
|
+
include Seams::Generators::EjectAware
|
|
45
|
+
|
|
46
|
+
source_root File.expand_path("templates", __dir__)
|
|
47
|
+
|
|
48
|
+
# Opt-in app shell (#26). Off by default — the design system generates the
|
|
49
|
+
# component library only. With --shell, the generator additionally writes a
|
|
50
|
+
# default application layout (header, nav, flash, footer, all built from
|
|
51
|
+
# ui_* components) and a starter signed-in dashboard + route into the host,
|
|
52
|
+
# so the host boots looking like a product. This is the building block the
|
|
53
|
+
# future `seams new` orchestrator passes.
|
|
54
|
+
class_option :shell, type: :boolean, default: false,
|
|
55
|
+
desc: "Also generate a default application layout + starter dashboard"
|
|
56
|
+
|
|
57
|
+
ENGINE_NAME = "design"
|
|
58
|
+
|
|
59
|
+
def create_base_engine
|
|
60
|
+
# The base EngineGenerator raises if engines/design/ already exists.
|
|
61
|
+
# Skip it on a re-run so a second `bin/seams design` is a no-op on the
|
|
62
|
+
# engine and simply re-applies the (idempotent) host wiring below.
|
|
63
|
+
if File.directory?(engine_path(""))
|
|
64
|
+
say " exist engines/design (kept — re-applying host wiring only)", :blue
|
|
65
|
+
return
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
EngineGenerator.start([ENGINE_NAME], destination_root: destination_root)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# The base EngineGenerator emits an ISOLATED engine. The design engine is
|
|
72
|
+
# non-isolated by design (D4), so overwrite lib/design/engine.rb with the
|
|
73
|
+
# non-isolated form that auto-wires the helper into ActionController::Base.
|
|
74
|
+
# engine.rb stays framework-managed (NOT eject-aware), like every other
|
|
75
|
+
# canonical generator's engine.rb.
|
|
76
|
+
def overwrite_engine_entry_point
|
|
77
|
+
template "lib/engine.rb.tt", engine_path("lib/design/engine.rb"), force: true
|
|
78
|
+
template "lib/design.rb.tt", engine_path("lib/design.rb"), force: true
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# The base scaffold ships an isolated engine's ApplicationController and
|
|
82
|
+
# ApplicationRecord under app/controllers/design/ and app/models/design/.
|
|
83
|
+
# A view-layer engine needs neither — its only controller is the dev-only
|
|
84
|
+
# guide (created below) and it has no models — so remove the leftovers.
|
|
85
|
+
def remove_isolated_leftovers
|
|
86
|
+
# config/routes.rb is KEPT (an empty `Design::Engine.routes.draw do end`)
|
|
87
|
+
# so the engine stays mountable in the dummy app + host; the dev-only
|
|
88
|
+
# guide route (#18) is drawn into it later.
|
|
89
|
+
%w[
|
|
90
|
+
app/controllers/design/application_controller.rb
|
|
91
|
+
app/models/design/application_record.rb
|
|
92
|
+
spec/design_spec.rb
|
|
93
|
+
].each do |relative|
|
|
94
|
+
full = engine_path(relative)
|
|
95
|
+
next unless File.exist?(full)
|
|
96
|
+
|
|
97
|
+
FileUtils.rm(full)
|
|
98
|
+
say " remove #{relative} (isolated-engine leftover)", :red
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
%w[app/controllers/design app/models/design].each do |relative|
|
|
102
|
+
full = engine_path(relative)
|
|
103
|
+
Dir.rmdir(full) if File.directory?(full) && Dir.empty?(full)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# The auto-wire registry: Design.component_names derives the public
|
|
108
|
+
# component list from the preview partials, and resets on reload so a new
|
|
109
|
+
# component appears without a server restart. Ported from quire-saas's
|
|
110
|
+
# lib/compositor.rb (compositor -> design, compositor/previews -> ui/previews).
|
|
111
|
+
def create_auto_wire
|
|
112
|
+
template "lib/design/components.rb.tt", engine_path("lib/design/components.rb")
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# The host-wide helper module. ui_icon is the one hand-written helper;
|
|
116
|
+
# define_component_helpers! auto-derives ui_<name> for every preview.
|
|
117
|
+
# Ported from quire-saas's app/helpers/compositor_helper.rb.
|
|
118
|
+
def create_helper
|
|
119
|
+
template "app/helpers/design/ui_helper.rb.tt",
|
|
120
|
+
engine_path("app/helpers/design/ui_helper.rb")
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# The default form builder. Subclasses the standard Rails builder and only
|
|
124
|
+
# ADDS ui_* methods, so it is safe as the app-wide default. Ported from
|
|
125
|
+
# quire-saas's app/form_builders/compositor/form_builder.rb (compositor_*
|
|
126
|
+
# -> ui_*, the field partial path compositor/field -> ui/field). #19
|
|
127
|
+
# fleshes out the textarea/select/submit helpers + the ui/field partial.
|
|
128
|
+
def create_form_builder
|
|
129
|
+
template "app/form_builders/design/form_builder.rb.tt",
|
|
130
|
+
engine_path("app/form_builders/design/form_builder.rb")
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# The form-input component set (#19): the building blocks Design::FormBuilder
|
|
134
|
+
# and hand-written forms render. Ported faithfully from quire-saas's
|
|
135
|
+
# compositor (compositor_* -> ui_*, quire copy neutralised in the previews):
|
|
136
|
+
#
|
|
137
|
+
# - field the label/input/hint/error wrapper with baked-in
|
|
138
|
+
# aria-invalid + aria-describedby wiring (what
|
|
139
|
+
# f.ui_text_field renders);
|
|
140
|
+
# - checkbox an accessible labelled checkbox with an optional hint;
|
|
141
|
+
# - radio a labelled radio (grouped by name in a fieldset);
|
|
142
|
+
# - switch a role="switch" toggle;
|
|
143
|
+
# - input_group a text input with an optional prefix/suffix affix.
|
|
144
|
+
#
|
|
145
|
+
# Each ships with a companion preview, which is what makes it "public": the
|
|
146
|
+
# auto-wire derives ui_<name> from the preview and the gallery lists it.
|
|
147
|
+
# Eject-aware so a host can own a component without losing it on regenerate.
|
|
148
|
+
def create_form_components
|
|
149
|
+
%w[field checkbox radio switch input_group].each do |name|
|
|
150
|
+
template_unless_ejected "app/views/ui/_#{name}.html.erb.tt",
|
|
151
|
+
engine_path("app/views/ui/_#{name}.html.erb")
|
|
152
|
+
template_unless_ejected "app/views/ui/previews/_#{name}.html.erb.tt",
|
|
153
|
+
engine_path("app/views/ui/previews/_#{name}.html.erb")
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# The icon sprite + icon partials — the minimum view surface the skeleton
|
|
158
|
+
# needs so `render "ui/icon_sprite"` (wired into the host layout below) and
|
|
159
|
+
# ui_icon resolve. #25 ships the full primitive + icon set; these two are
|
|
160
|
+
# the load-bearing pair the host layout references on first boot.
|
|
161
|
+
def create_icon_partials
|
|
162
|
+
template_unless_ejected "app/views/ui/_icon.html.erb.tt",
|
|
163
|
+
engine_path("app/views/ui/_icon.html.erb")
|
|
164
|
+
template_unless_ejected "app/views/ui/_icon_sprite.html.erb.tt",
|
|
165
|
+
engine_path("app/views/ui/_icon_sprite.html.erb")
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# The seed component set — enough for the gallery + the contract/render
|
|
169
|
+
# tests to have something real to render. Ported faithfully from
|
|
170
|
+
# quire-saas's compositor (compositor_* -> ui_*, quire copy neutralised):
|
|
171
|
+
# _button (takes a content block + variant/size) and _tag (a required
|
|
172
|
+
# `label:` strict local — the contract test relies on it being required).
|
|
173
|
+
# Each ships with a companion preview, which is what makes it "public":
|
|
174
|
+
# the auto-wire derives ui_<name> from the preview, and the gallery lists
|
|
175
|
+
# it. Eject-aware so a host can own a component without losing it on
|
|
176
|
+
# regenerate. #21+ ship the full component set.
|
|
177
|
+
def create_seed_components
|
|
178
|
+
%w[button tag].each do |name|
|
|
179
|
+
template_unless_ejected "app/views/ui/_#{name}.html.erb.tt",
|
|
180
|
+
engine_path("app/views/ui/_#{name}.html.erb")
|
|
181
|
+
template_unless_ejected "app/views/ui/previews/_#{name}.html.erb.tt",
|
|
182
|
+
engine_path("app/views/ui/previews/_#{name}.html.erb")
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# The Navigation component set (Phase 2, GROUPKEY = nav). Ported faithfully
|
|
187
|
+
# from quire-saas's compositor (compositor_* -> ui_*, quire copy
|
|
188
|
+
# neutralised in the previews), each carrying its baked-in navigation
|
|
189
|
+
# accessibility roles/aria:
|
|
190
|
+
#
|
|
191
|
+
# - breadcrumb a nav[aria-label=Breadcrumb] trail with aria-current=page;
|
|
192
|
+
# - pagination a nav[aria-label=Pagination] with per-page aria-current;
|
|
193
|
+
# - menu a role=menu list of role=menuitem links/buttons;
|
|
194
|
+
# - segmented a role=group of aria-pressed toggle buttons;
|
|
195
|
+
# - stepper an ordered list with aria-current=step + done ticks;
|
|
196
|
+
# - toolbar a role=toolbar of labelled icon/text buttons;
|
|
197
|
+
# - outline a nav[aria-label=Outline] heading tree with aria-current.
|
|
198
|
+
#
|
|
199
|
+
# Each ships with a companion preview, which is what makes it "public": the
|
|
200
|
+
# auto-wire derives ui_<name> from the preview and the gallery lists it.
|
|
201
|
+
# Eject-aware so a host can own a component without losing it on regenerate.
|
|
202
|
+
def create_nav_components
|
|
203
|
+
%w[breadcrumb pagination menu segmented stepper toolbar outline].each do |name|
|
|
204
|
+
template_unless_ejected "app/views/ui/_#{name}.html.erb.tt",
|
|
205
|
+
engine_path("app/views/ui/_#{name}.html.erb")
|
|
206
|
+
template_unless_ejected "app/views/ui/previews/_#{name}.html.erb.tt",
|
|
207
|
+
engine_path("app/views/ui/previews/_#{name}.html.erb")
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# The Overlays component set (Phase 2, GROUPKEY = overlays). Ported
|
|
212
|
+
# faithfully from quire-saas's compositor (compositor_* -> ui_*,
|
|
213
|
+
# compositor-dialog controller -> ui-dialog, quire copy neutralised in the
|
|
214
|
+
# previews), each carrying its baked-in overlay accessibility:
|
|
215
|
+
#
|
|
216
|
+
# - dialog a native <dialog aria-labelledby> with a labelled close
|
|
217
|
+
# button, driven by a ui-dialog Stimulus controller the host
|
|
218
|
+
# supplies (data-controller / data-action wiring baked in);
|
|
219
|
+
# - drawer an <aside aria-label> side-panel landmark;
|
|
220
|
+
# - popover a role=note annotation bubble;
|
|
221
|
+
# - savestate a role=status live region (saved / saving / unsaved).
|
|
222
|
+
#
|
|
223
|
+
# Each ships with a companion preview, which is what makes it "public": the
|
|
224
|
+
# auto-wire derives ui_<name> from the preview and the gallery lists it.
|
|
225
|
+
# Eject-aware so a host can own a component without losing it on regenerate.
|
|
226
|
+
def create_overlays_components
|
|
227
|
+
%w[dialog drawer popover savestate].each do |name|
|
|
228
|
+
template_unless_ejected "app/views/ui/_#{name}.html.erb.tt",
|
|
229
|
+
engine_path("app/views/ui/_#{name}.html.erb")
|
|
230
|
+
template_unless_ejected "app/views/ui/previews/_#{name}.html.erb.tt",
|
|
231
|
+
engine_path("app/views/ui/previews/_#{name}.html.erb")
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# The Primitives & icons component set (Phase 2, GROUPKEY = primitives).
|
|
236
|
+
# The icon + icon_sprite primitives ship from create_icon_partials above;
|
|
237
|
+
# this set adds the remaining low-level building blocks, ported faithfully
|
|
238
|
+
# from quire-saas's compositor (compositor_* -> ui_*, quire copy
|
|
239
|
+
# neutralised in the previews), each carrying its baked-in accessibility:
|
|
240
|
+
#
|
|
241
|
+
# - panel a plain raised content surface (a content-block wrapper);
|
|
242
|
+
# - diff a per-line add/del/ctx list whose +/- signs are aria-labelled
|
|
243
|
+
# "added"/"removed" so the glyph alone is not load-bearing;
|
|
244
|
+
# - empty an empty-state with a required title + content-block body.
|
|
245
|
+
#
|
|
246
|
+
# Each ships with a companion preview, which is what makes it "public": the
|
|
247
|
+
# auto-wire derives ui_<name> from the preview and the gallery lists it.
|
|
248
|
+
# Eject-aware so a host can own a component without losing it on regenerate.
|
|
249
|
+
def create_primitive_components
|
|
250
|
+
%w[panel diff empty].each do |name|
|
|
251
|
+
template_unless_ejected "app/views/ui/_#{name}.html.erb.tt",
|
|
252
|
+
engine_path("app/views/ui/_#{name}.html.erb")
|
|
253
|
+
template_unless_ejected "app/views/ui/previews/_#{name}.html.erb.tt",
|
|
254
|
+
engine_path("app/views/ui/previews/_#{name}.html.erb")
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# The Actions & status component set (Phase 2, GROUPKEY = actions). Ported
|
|
259
|
+
# faithfully from quire-saas's compositor (compositor_* -> ui_*, quire copy
|
|
260
|
+
# neutralised in the previews), each carrying its baked-in accessibility:
|
|
261
|
+
#
|
|
262
|
+
# - banner a page-level role=region announcement with a tone variant;
|
|
263
|
+
# - toast a role=status transient notification;
|
|
264
|
+
# - note an inline annotation span.
|
|
265
|
+
#
|
|
266
|
+
# Each ships with a companion preview, which is what makes it "public": the
|
|
267
|
+
# auto-wire derives ui_<name> from the preview and the gallery lists it.
|
|
268
|
+
# Eject-aware so a host can own a component without losing it on regenerate.
|
|
269
|
+
def create_actions_components
|
|
270
|
+
%w[banner toast note].each do |name|
|
|
271
|
+
template_unless_ejected "app/views/ui/_#{name}.html.erb.tt",
|
|
272
|
+
engine_path("app/views/ui/_#{name}.html.erb")
|
|
273
|
+
template_unless_ejected "app/views/ui/previews/_#{name}.html.erb.tt",
|
|
274
|
+
engine_path("app/views/ui/previews/_#{name}.html.erb")
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# The Data display component set (Phase 2, GROUPKEY = data). Ported
|
|
279
|
+
# faithfully from quire-saas's compositor (compositor_* -> ui_*, quire copy
|
|
280
|
+
# neutralised in the previews), each carrying its baked-in accessibility:
|
|
281
|
+
#
|
|
282
|
+
# - card a titled content surface;
|
|
283
|
+
# - data_table a <table> with a caption + scoped headers;
|
|
284
|
+
# - chapter_row a manuscript chapter list row;
|
|
285
|
+
# - build_row an export/build status list row;
|
|
286
|
+
# - counter a labelled numeric stat;
|
|
287
|
+
# - meter a <meter>-backed progress indicator;
|
|
288
|
+
# - kbd a <kbd> keyboard-shortcut glyph.
|
|
289
|
+
#
|
|
290
|
+
# Each ships with a companion preview, which is what makes it "public": the
|
|
291
|
+
# auto-wire derives ui_<name> from the preview and the gallery lists it.
|
|
292
|
+
# Eject-aware so a host can own a component without losing it on regenerate.
|
|
293
|
+
def create_data_components
|
|
294
|
+
%w[card data_table chapter_row build_row counter meter kbd].each do |name|
|
|
295
|
+
template_unless_ejected "app/views/ui/_#{name}.html.erb.tt",
|
|
296
|
+
engine_path("app/views/ui/_#{name}.html.erb")
|
|
297
|
+
template_unless_ejected "app/views/ui/previews/_#{name}.html.erb.tt",
|
|
298
|
+
engine_path("app/views/ui/previews/_#{name}.html.erb")
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# The living gallery (dev/test only). The controller renders every
|
|
303
|
+
# component from its preview so the docs cannot drift; the route is guarded
|
|
304
|
+
# to Rails.env.local? both in the controller (404 in production) and at the
|
|
305
|
+
# host routes (drawn inside an `if Rails.env.local?` block). Ported from
|
|
306
|
+
# quire-saas's compositor guide. Eject-aware so a host can restyle the
|
|
307
|
+
# gallery chrome.
|
|
308
|
+
def create_guide
|
|
309
|
+
template "app/controllers/design/guide_controller.rb.tt",
|
|
310
|
+
engine_path("app/controllers/design/guide_controller.rb")
|
|
311
|
+
template_unless_ejected "app/views/layouts/design/guide.html.erb.tt",
|
|
312
|
+
engine_path("app/views/layouts/design/guide.html.erb")
|
|
313
|
+
template_unless_ejected "app/views/design/guide/index.html.erb.tt",
|
|
314
|
+
engine_path("app/views/design/guide/index.html.erb")
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# The design:component generator (#20): ships INSIDE the generated engine
|
|
318
|
+
# at engines/design/lib/generators/design/component/, so a host can run
|
|
319
|
+
# `rails g design:component <name>` (Rails auto-discovers it on the engine's
|
|
320
|
+
# lib path; no registration needed). Ported from quire-saas's Compositor.
|
|
321
|
+
#
|
|
322
|
+
# copy_file (NOT template): the generator's own .tt templates are ERB run by
|
|
323
|
+
# `rails g design:component`, so they must reach the engine VERBATIM. The
|
|
324
|
+
# meta-generator's ERB would un-escape their `<%%` markers and nest the
|
|
325
|
+
# `<%= file_name %>` placeholders inside a real tag — a parse error.
|
|
326
|
+
def create_component_generator
|
|
327
|
+
copy_file "lib/generators/design/component/component_generator.rb.tt",
|
|
328
|
+
engine_path("lib/generators/design/component/component_generator.rb")
|
|
329
|
+
copy_file "lib/generators/design/component/templates/component.html.erb.tt",
|
|
330
|
+
engine_path("lib/generators/design/component/templates/component.html.erb.tt")
|
|
331
|
+
copy_file "lib/generators/design/component/templates/preview.html.erb.tt",
|
|
332
|
+
engine_path("lib/generators/design/component/templates/preview.html.erb.tt")
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def create_runtime_spec
|
|
336
|
+
template "spec/runtime/design_boot_spec.rb.tt",
|
|
337
|
+
engine_path("spec/runtime/design_boot_spec.rb")
|
|
338
|
+
template "spec/runtime/ui_components_spec.rb.tt",
|
|
339
|
+
engine_path("spec/runtime/ui_components_spec.rb")
|
|
340
|
+
template "spec/runtime/form_builder_spec.rb.tt",
|
|
341
|
+
engine_path("spec/runtime/form_builder_spec.rb")
|
|
342
|
+
template "spec/runtime/guide_spec.rb.tt",
|
|
343
|
+
engine_path("spec/runtime/guide_spec.rb")
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def overwrite_readme
|
|
347
|
+
template "README.md.tt", engine_path("README.md"), force: true
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# The opt-in app shell (#26), generated ONLY with --shell. Without the
|
|
351
|
+
# flag none of these files appear and the host keeps its rails-new layout.
|
|
352
|
+
#
|
|
353
|
+
# - app/views/layouts/application.html.erb — the HOST's default layout,
|
|
354
|
+
# overwritten (force) with one built entirely from ui_* components
|
|
355
|
+
# (header, nav, flash banners, footer). Eject-aware so a host that has
|
|
356
|
+
# already customised it on a later run keeps its version.
|
|
357
|
+
# - the starter signed-in dashboard controller + view, shipped INTO the
|
|
358
|
+
# engine (Design::DashboardController subclasses the host's
|
|
359
|
+
# ApplicationController), with an empty-state listing the engines the
|
|
360
|
+
# host could add. The route + root are drawn in wire_into_host.
|
|
361
|
+
def create_shell
|
|
362
|
+
return unless shell?
|
|
363
|
+
|
|
364
|
+
say " shell generating the opt-in app shell (--shell)", :green
|
|
365
|
+
create_shell_layout
|
|
366
|
+
template "app/controllers/design/dashboard_controller.rb.tt",
|
|
367
|
+
engine_path("app/controllers/design/dashboard_controller.rb")
|
|
368
|
+
template_unless_ejected "app/views/design/dashboard/index.html.erb.tt",
|
|
369
|
+
engine_path("app/views/design/dashboard/index.html.erb")
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
# Ship the example "quire" theme (#27) into the host as a token overlay the
|
|
373
|
+
# host can opt into. It is NOT applied by default (the neutral theme owns
|
|
374
|
+
# the default, per the proposal) — it sits alongside application.css as the
|
|
375
|
+
# worked proof that retheming is a token override: add one
|
|
376
|
+
# `@import "themes/quire";` line and the whole app reskins. The theming
|
|
377
|
+
# guide (doc/design-system/DESIGN_SYSTEM_THEMING.md) documents it. Eject-aware.
|
|
378
|
+
def create_example_theme
|
|
379
|
+
template_unless_ejected "app/assets/tailwind/themes/_quire.css",
|
|
380
|
+
host_path("app/assets/tailwind/themes/_quire.css")
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# --- Host wiring ----------------------------------------------------------
|
|
384
|
+
|
|
385
|
+
def wire_into_host
|
|
386
|
+
# Tailwind v4 is a hard dependency (D2): the @theme token layer the
|
|
387
|
+
# engine ships is Tailwind-native. Inject the gem and write the token
|
|
388
|
+
# block into the host's application.css.
|
|
389
|
+
host_inject_gem("tailwindcss-rails", "~> 4.0")
|
|
390
|
+
inject_theme_into_host_css
|
|
391
|
+
set_host_default_form_builder
|
|
392
|
+
render_sprite_in_host_layout
|
|
393
|
+
draw_guide_route_in_host
|
|
394
|
+
draw_dashboard_route_in_host if shell?
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def report_summary
|
|
398
|
+
say report_summary_text, :green
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
private
|
|
402
|
+
|
|
403
|
+
# Draw the dev/test-only living-gallery route into the HOST's routes. The
|
|
404
|
+
# design engine is non-isolated, so its Design::GuideController lives on the
|
|
405
|
+
# host's controller path and a plain host route reaches it — matching how
|
|
406
|
+
# quire-saas exposes /compositor/guide. The route is wrapped in an
|
|
407
|
+
# `if Rails.env.local?` guard so it does not exist in production at all
|
|
408
|
+
# (defence in depth with the controller's own guard_available? 404).
|
|
409
|
+
# Idempotent: skips if the route is already drawn.
|
|
410
|
+
def draw_guide_route_in_host
|
|
411
|
+
routes = host_path("config/routes.rb")
|
|
412
|
+
unless File.exist?(routes)
|
|
413
|
+
return host_skip("config/routes.rb not found — add the guide route " \
|
|
414
|
+
'(get "design/guide" => "design/guide#index") yourself')
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
return if File.read(routes).include?('"design/guide#index"')
|
|
418
|
+
|
|
419
|
+
say " inject config/routes.rb (design/guide — dev/test only)", :green
|
|
420
|
+
inject_into_file routes, after: routes_draw_anchor do
|
|
421
|
+
<<-RUBY
|
|
422
|
+
# The seams design living gallery — dev/test only. Renders every ui_*
|
|
423
|
+
# component from its preview so the docs cannot drift. Guarded here AND in
|
|
424
|
+
# the controller so it never reaches production.
|
|
425
|
+
if Rails.env.local?
|
|
426
|
+
get "design/guide" => "design/guide#index", as: :design_guide
|
|
427
|
+
end
|
|
428
|
+
RUBY
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def engine_path(relative)
|
|
433
|
+
File.join(destination_root, "engines", ENGINE_NAME, relative)
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def shell?
|
|
437
|
+
options[:shell]
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
# A human app name for the shell layout + dashboard copy, derived from the
|
|
441
|
+
# host's config/application.rb module (the `rails new` app name), falling
|
|
442
|
+
# back to a sensible default. Pure cosmetics — the host owns these files.
|
|
443
|
+
def app_name
|
|
444
|
+
@app_name ||= begin
|
|
445
|
+
application_rb = host_path("config/application.rb")
|
|
446
|
+
name = File.read(application_rb)[/module\s+([A-Z]\w+)/, 1] if File.exist?(application_rb)
|
|
447
|
+
(name || "App").gsub(/([a-z])([A-Z])/, '\1 \2')
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
# Write the HOST's application layout from the shell template. force: true
|
|
452
|
+
# because `rails new` already shipped a default application.html.erb we are
|
|
453
|
+
# deliberately replacing; eject-aware so a host that has stamped the eject
|
|
454
|
+
# header (to own its layout) keeps its version on a later regenerate.
|
|
455
|
+
def create_shell_layout
|
|
456
|
+
template_unless_ejected "app/views/layouts/application.html.erb.tt",
|
|
457
|
+
host_path("app/views/layouts/application.html.erb"),
|
|
458
|
+
force: true
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
# Draw the starter dashboard route into the HOST routes and point root at
|
|
462
|
+
# it, so a --shell host boots straight to the styled dashboard. The design
|
|
463
|
+
# engine is non-isolated, so Design::DashboardController lives on the host
|
|
464
|
+
# controller path and a plain host route reaches it. Idempotent: skips if
|
|
465
|
+
# the dashboard route is already drawn.
|
|
466
|
+
def draw_dashboard_route_in_host
|
|
467
|
+
routes = host_path("config/routes.rb")
|
|
468
|
+
unless File.exist?(routes)
|
|
469
|
+
return host_skip("config/routes.rb not found — add the dashboard route " \
|
|
470
|
+
'(root "design/dashboard#index") yourself')
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
return if File.read(routes).include?('"design/dashboard#index"')
|
|
474
|
+
|
|
475
|
+
say " inject config/routes.rb (starter dashboard + root)", :green
|
|
476
|
+
# Only add a root route if the host has none yet — never clobber a
|
|
477
|
+
# host-defined root.
|
|
478
|
+
root_line = File.read(routes).match?(/^\s*root\s/) ? "" : %( root "design/dashboard#index"\n)
|
|
479
|
+
block = <<~RUBY
|
|
480
|
+
# The seams design starter dashboard (--shell). A styled, signed-in home the
|
|
481
|
+
# host boots to; replace Design::DashboardController with your real home page.
|
|
482
|
+
get "dashboard" => "design/dashboard#index", as: :dashboard
|
|
483
|
+
RUBY
|
|
484
|
+
block = block.gsub(/^/, " ") + root_line
|
|
485
|
+
inject_into_file routes, after: routes_draw_anchor do
|
|
486
|
+
block
|
|
487
|
+
end
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
# Write the neutral @theme token layer into the host's Tailwind entrypoint.
|
|
491
|
+
# This is the SINGLE SOURCE every ui_* component reads (#17): the full,
|
|
492
|
+
# WCAG-AA-audited neutral default — the @theme palette/type tokens, the
|
|
493
|
+
# `:root` alias layer (type scale, spacing, radius, shadow, motion, layout,
|
|
494
|
+
# z-index, breakpoints) and the base focus/selection/skip-link rules. The
|
|
495
|
+
# block lives in templates/app/assets/tailwind/_tokens.css so it stays
|
|
496
|
+
# readable and diffable; the generator appends it verbatim.
|
|
497
|
+
#
|
|
498
|
+
# Also adds an `@source` line so Tailwind scans the engine's component
|
|
499
|
+
# partials and builds the utility classes they emit. If the host has no
|
|
500
|
+
# application.css yet (no tailwindcss-rails installed at generate time),
|
|
501
|
+
# create one with the `@import "tailwindcss"` line so the first boot has a
|
|
502
|
+
# working stylesheet. Idempotent — skips if the token marker is present.
|
|
503
|
+
def inject_theme_into_host_css
|
|
504
|
+
css_path = host_path("app/assets/tailwind/application.css")
|
|
505
|
+
|
|
506
|
+
unless File.exist?(css_path)
|
|
507
|
+
FileUtils.mkdir_p(File.dirname(css_path))
|
|
508
|
+
create_file css_path, host_css_preamble
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
# Ensure Tailwind scans the engine's ui/ partials even when the host
|
|
512
|
+
# already had its own application.css (e.g. after tailwindcss:install).
|
|
513
|
+
unless File.read(css_path).include?(ENGINE_SOURCE_GLOB)
|
|
514
|
+
append_to_file css_path, <<~CSS
|
|
515
|
+
|
|
516
|
+
/* Scan the seams design engine so the classes its ui/ partials emit are built. */
|
|
517
|
+
@source "#{ENGINE_SOURCE_GLOB}";
|
|
518
|
+
CSS
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
return if File.read(css_path).include?(THEME_MARKER)
|
|
522
|
+
|
|
523
|
+
say " inject app/assets/tailwind/application.css (@theme tokens)", :green
|
|
524
|
+
append_to_file css_path, "\n#{neutral_theme_block}"
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
ENGINE_SOURCE_GLOB = "../../../engines/design/app/views"
|
|
528
|
+
private_constant :ENGINE_SOURCE_GLOB
|
|
529
|
+
|
|
530
|
+
THEME_MARKER = "seams:design tokens"
|
|
531
|
+
private_constant :THEME_MARKER
|
|
532
|
+
|
|
533
|
+
# The Tailwind entrypoint we create when the host has none yet: the import
|
|
534
|
+
# plus an @source line so Tailwind scans the engine's ui/ partials and
|
|
535
|
+
# builds the utility classes they emit.
|
|
536
|
+
def host_css_preamble
|
|
537
|
+
<<~CSS
|
|
538
|
+
@import "tailwindcss";
|
|
539
|
+
|
|
540
|
+
/* Scan the seams design engine so the classes its ui/ partials emit are built. */
|
|
541
|
+
@source "#{ENGINE_SOURCE_GLOB}";
|
|
542
|
+
CSS
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
# The full neutral default token layer (#17), read verbatim from the
|
|
546
|
+
# template so the large CSS stays readable and reviewable in one place.
|
|
547
|
+
def neutral_theme_block
|
|
548
|
+
File.read(File.expand_path("templates/app/assets/tailwind/_tokens.css", __dir__))
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
# Make Design::FormBuilder the host's default form builder so every
|
|
552
|
+
# `form_with` / `form_for` gets the f.ui_* field methods without passing
|
|
553
|
+
# `builder:`. Injected into config/application.rb inside the Application
|
|
554
|
+
# class body. Idempotent.
|
|
555
|
+
def set_host_default_form_builder
|
|
556
|
+
application_rb = host_path("config/application.rb")
|
|
557
|
+
unless File.exist?(application_rb)
|
|
558
|
+
return host_skip("config/application.rb not found — set " \
|
|
559
|
+
"config.action_view.default_form_builder = \"Design::FormBuilder\" yourself")
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
contents = File.read(application_rb)
|
|
563
|
+
return if contents.include?("default_form_builder")
|
|
564
|
+
|
|
565
|
+
say " inject config/application.rb (default_form_builder = Design::FormBuilder)", :green
|
|
566
|
+
inject_into_class application_rb, "Application", <<~RUBY
|
|
567
|
+
# The Design engine's FormBuilder only ADDS ui_* field helpers; the
|
|
568
|
+
# standard f.text_field / f.select / f.submit are untouched, so it is
|
|
569
|
+
# safe as the app-wide default. Set as a string so the constant is
|
|
570
|
+
# resolved lazily, after the engine has loaded.
|
|
571
|
+
config.action_view.default_form_builder = "Design::FormBuilder"
|
|
572
|
+
RUBY
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
# Render the icon sprite once near the top of <body> in the host layout so
|
|
576
|
+
# the ui_* components can reference icons by fragment without an external
|
|
577
|
+
# request. Injected immediately after the opening <body> tag. Skips if the
|
|
578
|
+
# host layout is missing or already renders the sprite.
|
|
579
|
+
def render_sprite_in_host_layout
|
|
580
|
+
layout = host_path("app/views/layouts/application.html.erb")
|
|
581
|
+
unless File.exist?(layout)
|
|
582
|
+
return host_skip("app/views/layouts/application.html.erb not found — " \
|
|
583
|
+
'add `<%= render "ui/icon_sprite" %>` near the top of <body> yourself')
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
contents = File.read(layout)
|
|
587
|
+
return if contents.include?('render "ui/icon_sprite"')
|
|
588
|
+
|
|
589
|
+
body_anchor = /<body[^>]*>\n/
|
|
590
|
+
unless contents.match?(body_anchor)
|
|
591
|
+
return host_skip("app/views/layouts/application.html.erb has no <body> tag — " \
|
|
592
|
+
'add `<%= render "ui/icon_sprite" %>` near the top of <body> yourself')
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
say " inject app/views/layouts/application.html.erb (render \"ui/icon_sprite\")", :green
|
|
596
|
+
inject_into_file layout, after: body_anchor do
|
|
597
|
+
%( <%# seams design — icon sprite for ui_* components %>\n) +
|
|
598
|
+
%( <%= render "ui/icon_sprite" %>\n)
|
|
599
|
+
end
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
def shell_summary_note
|
|
603
|
+
return "" unless shell?
|
|
604
|
+
|
|
605
|
+
<<~SHELL
|
|
606
|
+
|
|
607
|
+
App shell (--shell): a default app/views/layouts/application.html.erb
|
|
608
|
+
and a starter dashboard at root (and /dashboard) were generated.
|
|
609
|
+
Boot the host and you land on the styled dashboard.
|
|
610
|
+
SHELL
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
def report_summary_text
|
|
614
|
+
<<~TXT
|
|
615
|
+
|
|
616
|
+
Design engine generated at engines/design/
|
|
617
|
+
#{shell_summary_note}
|
|
618
|
+
|
|
619
|
+
Next steps:
|
|
620
|
+
1. bundle install
|
|
621
|
+
(picks up tailwindcss-rails, injected into the host Gemfile)
|
|
622
|
+
|
|
623
|
+
2. bin/rails tailwindcss:install (if Tailwind isn't set up yet)
|
|
624
|
+
then build it: bin/rails tailwindcss:build
|
|
625
|
+
|
|
626
|
+
3. Use the components anywhere in the host or another engine's views:
|
|
627
|
+
<%= ui_button(variant: :primary) { "Save" } %>
|
|
628
|
+
<%= form_with model: @record do |f| %>
|
|
629
|
+
<%= f.ui_text_field :title, label: "Title" %>
|
|
630
|
+
<% end %>
|
|
631
|
+
|
|
632
|
+
The engine is non-isolated: ui_* helpers and ui/ partials resolve
|
|
633
|
+
everywhere, and Design::FormBuilder is the host default form builder.
|
|
634
|
+
|
|
635
|
+
Retheme by overriding the @theme tokens in
|
|
636
|
+
app/assets/tailwind/application.css. The example "quire" theme ships at
|
|
637
|
+
app/assets/tailwind/themes/_quire.css — apply it with one line:
|
|
638
|
+
`@import "themes/quire";` (see doc/design-system/DESIGN_SYSTEM_THEMING.md).
|
|
639
|
+
|
|
640
|
+
Run the engine specs:
|
|
641
|
+
bin/rails seams:test[design]
|
|
642
|
+
|
|
643
|
+
TXT
|
|
644
|
+
end
|
|
645
|
+
end
|
|
646
|
+
end
|
|
647
|
+
end
|