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,14 @@
|
|
|
1
|
+
<%%# locals: () -%>
|
|
2
|
+
<%%= ui_data_table(
|
|
3
|
+
columns: [
|
|
4
|
+
{ label: "Invoice" },
|
|
5
|
+
{ label: "Date", sortable: true, sort: "descending" },
|
|
6
|
+
{ label: "Status" },
|
|
7
|
+
{ label: "Amount", num: true, sortable: true, sort: "none" }
|
|
8
|
+
],
|
|
9
|
+
rows: [
|
|
10
|
+
["INV-2031", "12 Jun 2026", ui_tag(label: "Paid", status: :ready), "$1,240.00"],
|
|
11
|
+
["INV-2030", "28 May 2026", ui_tag(label: "Pending", status: :progress), "$860.50"],
|
|
12
|
+
["INV-2029", "14 May 2026", ui_tag(label: "Overdue", status: :alert), "$2,015.75"]
|
|
13
|
+
]
|
|
14
|
+
) %>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<%%# locals: () -%>
|
|
2
|
+
<%%= ui_dialog(id: "guide-del", title: "Delete this item?", trigger: "Delete item…", destructive: true) do %>
|
|
3
|
+
<p class="small muted mb-4">This removes the item and everything inside it. It can't be undone.</p>
|
|
4
|
+
<div class="row-wrap" style="justify-content:flex-end">
|
|
5
|
+
<%%= ui_button(data: { action: "ui-dialog#close" }) { "Keep it" } %>
|
|
6
|
+
<%%= ui_button(variant: :danger) { "Delete permanently" } %>
|
|
7
|
+
</div>
|
|
8
|
+
<%% end %>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<%%# locals: () -%>
|
|
2
|
+
<div class="stack">
|
|
3
|
+
<%%= ui_meter(value: 38400, max: 60000, state: :under, label: "Usage") %>
|
|
4
|
+
<%%= ui_meter(value: 60200, max: 60000, state: :at, label: "Usage") %>
|
|
5
|
+
<%%= ui_meter(value: 71900, max: 60000, state: :over, label: "Usage") %>
|
|
6
|
+
</div>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<%%# locals: () -%>
|
|
2
|
+
<%%= ui_outline(items: [
|
|
3
|
+
{ label: "Getting started", href: "#", level: 1, current: true },
|
|
4
|
+
{ label: "Installation", href: "#", level: 2 },
|
|
5
|
+
{ label: "Configuration", href: "#", level: 2 },
|
|
6
|
+
{ label: "A worked example", href: "#", level: 3 },
|
|
7
|
+
{ label: "Reference", href: "#", level: 1 }
|
|
8
|
+
]) %>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<%%# locals: () -%>
|
|
2
|
+
<div class="stack">
|
|
3
|
+
<%%= ui_segmented(
|
|
4
|
+
name: "view",
|
|
5
|
+
label: "View",
|
|
6
|
+
selected: "list",
|
|
7
|
+
options: [["list", "List"], ["grid", "Grid"], ["board", "Board"]]
|
|
8
|
+
) %>
|
|
9
|
+
<%%= ui_segmented(
|
|
10
|
+
name: "theme",
|
|
11
|
+
label: "Theme",
|
|
12
|
+
selected: "system",
|
|
13
|
+
options: [["light", "Light"], ["dark", "Dark"], ["system", "System"]]
|
|
14
|
+
) %>
|
|
15
|
+
</div>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<%%# locals: () -%>
|
|
2
|
+
<div class="stack">
|
|
3
|
+
<%%= ui_stepper(steps: ["Account", "Details", "Plan", "Payment", "Review"], current: 3) %>
|
|
4
|
+
<%%= ui_stepper(steps: ["Account", "Details", "Plan", "Payment", "Review"], current: 1) %>
|
|
5
|
+
<%%= ui_stepper(steps: ["Account", "Details", "Plan", "Payment", "Review"], current: 5) %>
|
|
6
|
+
</div>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<%%# locals: () -%>
|
|
2
|
+
<div class="row-wrap">
|
|
3
|
+
<%%= ui_tag(label: "Ready", status: :ready) %>
|
|
4
|
+
<%%= ui_tag(label: "Building", status: :progress) %>
|
|
5
|
+
<%%= ui_tag(label: "Failed", status: :alert) %>
|
|
6
|
+
<%%= ui_tag(label: "Draft", status: :neutral) %>
|
|
7
|
+
<%%= ui_tag(label: "No dot", status: :neutral, dot: false) %>
|
|
8
|
+
</div>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<%%# locals: () -%>
|
|
2
|
+
<div class="toaster" style="position:static;width:340px;max-width:100%">
|
|
3
|
+
<%%= ui_toast(message: "Changes saved", kind: :ok) %>
|
|
4
|
+
<%%= ui_toast(message: "Couldn't save — check your connection", kind: :alert) %>
|
|
5
|
+
<%%= ui_toast(message: "Copied to clipboard", kind: :info) %>
|
|
6
|
+
</div>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Design
|
|
4
|
+
class << self
|
|
5
|
+
# Component names, derived from the preview partials under
|
|
6
|
+
# app/views/ui/previews/. Memoised; reset on code reload via the engine's
|
|
7
|
+
# to_prepare hook so new components appear without a server restart.
|
|
8
|
+
def component_names
|
|
9
|
+
@component_names ||= Dir[previews_root.join("_*.html.erb")]
|
|
10
|
+
.map { |path| File.basename(path).delete_prefix("_").delete_suffix(".html.erb") }
|
|
11
|
+
.sort
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def reset_component_names!
|
|
15
|
+
@component_names = nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def previews_root
|
|
19
|
+
Engine.root.join("app", "views", "ui", "previews")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "design/version"
|
|
4
|
+
require "design/components"
|
|
5
|
+
require "design/engine"
|
|
6
|
+
|
|
7
|
+
# Design — the seams design system, as a callable component library.
|
|
8
|
+
#
|
|
9
|
+
# A component is a strict-locals ERB partial under app/views/ui/ that has a
|
|
10
|
+
# companion preview under app/views/ui/previews/. The preview is what makes a
|
|
11
|
+
# component "public": it gives the auto-wired `ui_<name>` helper and the living
|
|
12
|
+
# guide their list. Internal partials (icon, icon_sprite) have no preview, so
|
|
13
|
+
# they get no helper and never appear in the guide.
|
|
14
|
+
#
|
|
15
|
+
# Plain ERB by design — the whole surface stays inside the host's Herb + RuboCop
|
|
16
|
+
# gate, with no view framework and no extra dependency.
|
|
17
|
+
module Design
|
|
18
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Design
|
|
4
|
+
# A deliberately NON-isolated engine (proposals/design_system_engine.md, D4):
|
|
5
|
+
# its app/views/ui partials and its UiHelper must be visible to the host AND
|
|
6
|
+
# to every other engine's views, so a component can be invoked anywhere. Rails
|
|
7
|
+
# appends the engine's app/views to the application's view paths automatically;
|
|
8
|
+
# the helper (and the auto-derived ui_<name> component helpers) are wired into
|
|
9
|
+
# ActionController::Base explicitly below.
|
|
10
|
+
#
|
|
11
|
+
# This engine intentionally does NOT isolate its namespace — that is the whole
|
|
12
|
+
# point. An isolated engine would scope the partials and keep the helper out of
|
|
13
|
+
# the host's controllers, which is exactly what this engine must NOT do.
|
|
14
|
+
class Engine < ::Rails::Engine
|
|
15
|
+
config.generators do |g|
|
|
16
|
+
g.test_framework :rspec
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
initializer "design.helpers" do |app|
|
|
20
|
+
app.config.to_prepare do
|
|
21
|
+
# Re-derive on every reload so a newly added component (a new preview
|
|
22
|
+
# partial under app/views/ui/previews/) gets its ui_<name> helper
|
|
23
|
+
# without a server restart.
|
|
24
|
+
Design.reset_component_names!
|
|
25
|
+
Design::UiHelper.define_component_helpers!
|
|
26
|
+
ActionController::Base.helper(Design::UiHelper)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
initializer "design.append_migrations" do |app|
|
|
31
|
+
unless app.root == root
|
|
32
|
+
config.paths["db/migrate"].expanded.each do |expanded_path|
|
|
33
|
+
app.config.paths["db/migrate"] << expanded_path
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
data/lib/generators/seams/design/templates/lib/generators/design/component/component_generator.rb.tt
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/named_base"
|
|
4
|
+
|
|
5
|
+
module Design
|
|
6
|
+
module Generators
|
|
7
|
+
# rails g design:component badge
|
|
8
|
+
#
|
|
9
|
+
# Scaffolds a component (a strict-locals partial under app/views/ui/) and its
|
|
10
|
+
# preview (under app/views/ui/previews/). The preview is what makes the
|
|
11
|
+
# component "public": it gets an auto-wired ui_<name> helper AND a section in
|
|
12
|
+
# the living gallery, both automatically, with no extra wiring. The generator
|
|
13
|
+
# gives you the skeleton and the conventions; you fill in the markup with the
|
|
14
|
+
# design system's Tailwind classes.
|
|
15
|
+
#
|
|
16
|
+
# Ported from quire-saas's Compositor (compositor:component), generalised:
|
|
17
|
+
# compositor/ -> ui/, compositor_<name> -> ui_<name>, Compositor:: -> Design::.
|
|
18
|
+
class ComponentGenerator < Rails::Generators::NamedBase
|
|
19
|
+
source_root File.expand_path("templates", __dir__)
|
|
20
|
+
|
|
21
|
+
desc "Scaffold a design-system component partial (app/views/ui/) and its preview."
|
|
22
|
+
|
|
23
|
+
def create_component_partial
|
|
24
|
+
template "component.html.erb.tt", view_path("ui/_#{file_name}.html.erb")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def create_preview_partial
|
|
28
|
+
template "preview.html.erb.tt", view_path("ui/previews/_#{file_name}.html.erb")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def next_steps
|
|
32
|
+
say ""
|
|
33
|
+
say " design:component #{file_name} scaffolded.", :green
|
|
34
|
+
say " 1. Build the markup in app/views/ui/_#{file_name}.html.erb"
|
|
35
|
+
say " using the design system's classes; bake in accessibility."
|
|
36
|
+
say " 2. Show its states in app/views/ui/previews/_#{file_name}.html.erb"
|
|
37
|
+
say " 3. ui_#{file_name} is now callable in every view; see /design/guide."
|
|
38
|
+
say ""
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
# The design engine is non-isolated, so its partials live in the engine's
|
|
44
|
+
# own app/views/ui/ tree (which Rails appends to the host's view paths).
|
|
45
|
+
# Resolve the engine root at runtime so the generator writes into the
|
|
46
|
+
# engine wherever the host vendored it.
|
|
47
|
+
def view_path(relative)
|
|
48
|
+
File.join(Design::Engine.root, "app", "views", relative)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<%%# locals: (variant: :default, content: nil, **attrs) -%>
|
|
2
|
+
<%%# TODO: build the <%= file_name %> component from the design system's classes.
|
|
3
|
+
Bake in accessibility (roles, aria-*, label association). Splat **attrs onto
|
|
4
|
+
the root element so callers can pass id / data-* / aria-* without forking. -%>
|
|
5
|
+
<%%= tag.div class: class_names("<%= file_name %>", "<%= file_name %>-#{variant}" => variant.to_s != "default"), **attrs do %><%%= content %><%% end %>
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../rails_helper"
|
|
4
|
+
|
|
5
|
+
# Boot spec for the seams design engine — Phase 1 (skeleton). Covers:
|
|
6
|
+
#
|
|
7
|
+
# - the engine constant loads and is NOT isolated (proposals/
|
|
8
|
+
# design_system_engine.md, D4)
|
|
9
|
+
# - the auto-wire registry (Design.component_names) derives the public
|
|
10
|
+
# component list from the preview partials and resets on demand
|
|
11
|
+
# - the host-wide UiHelper loads and is mixed into ActionController::Base,
|
|
12
|
+
# so ui_* helpers resolve in host + engine views
|
|
13
|
+
# - Design::FormBuilder loads and subclasses the standard Rails builder
|
|
14
|
+
# (so it is safe as the app-wide default)
|
|
15
|
+
# - the icon sprite + icon partials live in the engine's view path
|
|
16
|
+
#
|
|
17
|
+
# Later sub-issues add the token, component, gallery and FormBuilder-field
|
|
18
|
+
# specs; this proves the load-bearing skeleton boots.
|
|
19
|
+
RSpec.describe "Design engine boot", type: :integration do
|
|
20
|
+
describe "the engine constant" do
|
|
21
|
+
it "loads" do
|
|
22
|
+
expect(defined?(Design::Engine)).to eq("constant")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "is NON-isolated (no isolated namespace)" do
|
|
26
|
+
# An isolated engine sets Engine.isolated? true and scopes its helpers /
|
|
27
|
+
# routes. The design engine must NOT — its partials + helpers are host-wide.
|
|
28
|
+
expect(Design::Engine.isolated?).to be(false)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
describe "the auto-wire registry" do
|
|
33
|
+
it "derives the public component list from the preview partials" do
|
|
34
|
+
# The seed set (button, tag) each ships a preview, so they appear in the
|
|
35
|
+
# public list; the internal icon partials have no preview, so they don't.
|
|
36
|
+
expect(Design.component_names).to be_an(Array)
|
|
37
|
+
expect(Design.component_names).to include("button", "tag")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "auto-derives a ui_<name> helper for every public component" do
|
|
41
|
+
Design::UiHelper.define_component_helpers!
|
|
42
|
+
helpers = ActionController::Base.helpers
|
|
43
|
+
expect(helpers).to respond_to(:ui_button)
|
|
44
|
+
expect(helpers).to respond_to(:ui_tag)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "resets the memoised component list on demand" do
|
|
48
|
+
Design.component_names
|
|
49
|
+
expect { Design.reset_component_names! }.not_to raise_error
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
describe "the host-wide UiHelper" do
|
|
54
|
+
it "exposes the hand-written ui_icon" do
|
|
55
|
+
expect(defined?(Design::UiHelper)).to eq("constant")
|
|
56
|
+
expect(Design::UiHelper.instance_methods).to include(:ui_icon)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "is mixed into ActionController::Base so ui_* resolve host-wide" do
|
|
60
|
+
expect(ActionController::Base.helpers).to respond_to(:ui_icon)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
describe "Design::FormBuilder" do
|
|
65
|
+
it "subclasses the standard Rails builder" do
|
|
66
|
+
expect(defined?(Design::FormBuilder)).to eq("constant")
|
|
67
|
+
expect(Design::FormBuilder.ancestors).to include(ActionView::Helpers::FormBuilder)
|
|
68
|
+
expect(Design::FormBuilder.instance_methods).to include(:ui_text_field)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
describe "the engine view path" do
|
|
73
|
+
it "ships the icon sprite + icon partials" do
|
|
74
|
+
root = Design::Engine.root
|
|
75
|
+
expect(File.exist?(root.join("app/views/ui/_icon.html.erb"))).to be(true)
|
|
76
|
+
expect(File.exist?(root.join("app/views/ui/_icon_sprite.html.erb"))).to be(true)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../rails_helper"
|
|
4
|
+
|
|
5
|
+
# Design::FormBuilder — the host default builder (#19). Renders real forms
|
|
6
|
+
# through the dummy app's view stack so the ui/field partial + the aria/error
|
|
7
|
+
# wiring are exercised end to end, not stubbed.
|
|
8
|
+
#
|
|
9
|
+
# What it guards:
|
|
10
|
+
#
|
|
11
|
+
# - Design::FormBuilder is the app-wide default, so `form_with` yields it
|
|
12
|
+
# without passing `builder:` — f.ui_text_field is available;
|
|
13
|
+
# - f.ui_text_field renders the ui/field markup: an associated <label>, a
|
|
14
|
+
# value-bound .input, and (when given) a hint linked via aria-describedby;
|
|
15
|
+
# - on a model with a validation error, the field switches to the error state:
|
|
16
|
+
# aria-invalid="true", the error message, and aria-describedby pointing at
|
|
17
|
+
# the error node instead of the hint.
|
|
18
|
+
RSpec.describe "Design::FormBuilder", type: :integration do
|
|
19
|
+
# A minimal ActiveModel object so we can drive validations + errors through a
|
|
20
|
+
# real form, with no schema or table.
|
|
21
|
+
let(:form_model_class) do
|
|
22
|
+
Class.new do
|
|
23
|
+
include ActiveModel::Model
|
|
24
|
+
attr_accessor :title
|
|
25
|
+
|
|
26
|
+
validates :title, presence: true
|
|
27
|
+
|
|
28
|
+
def self.name = "Project"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Render a form the way a view would, through ApplicationController so the
|
|
33
|
+
# host-default builder + the engine's ui/ partials resolve. An inline template
|
|
34
|
+
# is the reliable way to exercise form_with's block + the ui/field partial
|
|
35
|
+
# outside a request.
|
|
36
|
+
def render_form(model)
|
|
37
|
+
ApplicationController.render(
|
|
38
|
+
inline: <<~ERB,
|
|
39
|
+
<%%= form_with(model: @model, url: "/", builder: Design::FormBuilder) do |form| %>
|
|
40
|
+
<%%= form.ui_text_field :title, label: "Title", hint: "Shown everywhere." %>
|
|
41
|
+
<%% end %>
|
|
42
|
+
ERB
|
|
43
|
+
assigns: { model: model }
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
describe "the default builder" do
|
|
48
|
+
it "only ADDS ui_* helpers — it is safe to make the app-wide default" do
|
|
49
|
+
# The standard field helpers are untouched, which is what makes it safe to
|
|
50
|
+
# set as config.action_view.default_form_builder (the generator does this
|
|
51
|
+
# in the host's config/application.rb; covered by the generator spec).
|
|
52
|
+
expect(Design::FormBuilder.ancestors).to include(ActionView::Helpers::FormBuilder)
|
|
53
|
+
expect(Design::FormBuilder.instance_methods).to include(:ui_text_field, :ui_submit)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "renders an associated label + value-bound input" do
|
|
57
|
+
model = form_model_class.new(title: "Acme")
|
|
58
|
+
html = render_form(model)
|
|
59
|
+
|
|
60
|
+
expect(html).to include(">Title</label>")
|
|
61
|
+
expect(html).to include('class="input"')
|
|
62
|
+
expect(html).to include('value="Acme"')
|
|
63
|
+
# the label's `for` targets the input's id (association is not by guesswork)
|
|
64
|
+
label_for = html[/<label[^>]*for="([^"]+)"/, 1]
|
|
65
|
+
expect(html).to include(%(id="#{label_for}"))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it "links the hint via aria-describedby when there is no error" do
|
|
69
|
+
model = form_model_class.new(title: "Acme")
|
|
70
|
+
html = render_form(model)
|
|
71
|
+
|
|
72
|
+
hint_id = html[/<p[^>]*class="hint"[^>]*id="([^"]+)"/, 1]
|
|
73
|
+
expect(hint_id).to end_with("-hint")
|
|
74
|
+
expect(html).to include(%(aria-describedby="#{hint_id}"))
|
|
75
|
+
expect(html).to include("Shown everywhere.")
|
|
76
|
+
expect(html).not_to include('aria-invalid="true"')
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
describe "the error state" do
|
|
81
|
+
it "wires aria-invalid + aria-describedby to the error, and drops the hint" do
|
|
82
|
+
model = form_model_class.new(title: nil)
|
|
83
|
+
model.validate
|
|
84
|
+
html = render_form(model)
|
|
85
|
+
|
|
86
|
+
expect(html).to include('aria-invalid="true"')
|
|
87
|
+
# the message comes from the model's errors, rendered into the error node
|
|
88
|
+
error_id = html[/<p[^>]*class="hint"[^>]*id="([^"]+)"/, 1]
|
|
89
|
+
expect(error_id).to end_with("-error")
|
|
90
|
+
expect(html).to include(%(aria-describedby="#{error_id}"))
|
|
91
|
+
expect(html).to include(model.errors[:title].first)
|
|
92
|
+
# in the error state the hint is suppressed, so describedby points at the
|
|
93
|
+
# error rather than the hint
|
|
94
|
+
expect(html).not_to include("Shown everywhere.")
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../rails_helper"
|
|
4
|
+
|
|
5
|
+
# The living-gallery smoke test. Drives the real /design/guide route through the
|
|
6
|
+
# dummy app's full Rack stack, so a component that breaks its preview (or the
|
|
7
|
+
# guide chrome itself) fails CI. Also proves the route is dev/test-only.
|
|
8
|
+
RSpec.describe "Design living gallery", type: :request do
|
|
9
|
+
describe "GET /design/guide" do
|
|
10
|
+
it "renders the gallery from the real previews" do
|
|
11
|
+
get "/design/guide"
|
|
12
|
+
|
|
13
|
+
expect(response).to have_http_status(:ok)
|
|
14
|
+
# The seed components appear, each rendered from its own preview.
|
|
15
|
+
expect(response.body).to include("ui_button")
|
|
16
|
+
expect(response.body).to include("ui_tag")
|
|
17
|
+
# The button preview's content proves the partial actually rendered.
|
|
18
|
+
expect(response.body).to include("Primary")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it "lists exactly the public component set" do
|
|
22
|
+
get "/design/guide"
|
|
23
|
+
|
|
24
|
+
Design.component_names.each do |name|
|
|
25
|
+
expect(response.body).to include("ui_#{name}")
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
describe "production guard" do
|
|
31
|
+
it "is unavailable in production (the controller 404s)" do
|
|
32
|
+
# The controller's before_action heads :not_found unless Rails.env.local?.
|
|
33
|
+
# We assert the guard exists rather than reload the whole app into the
|
|
34
|
+
# production environment inside the suite.
|
|
35
|
+
source = Design::Engine.root.join("app/controllers/design/guide_controller.rb").read
|
|
36
|
+
expect(source).to include("Rails.env.local?")
|
|
37
|
+
expect(source).to include("head :not_found")
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../rails_helper"
|
|
4
|
+
|
|
5
|
+
# The strict-locals contract + a per-component render smoke test for the seed
|
|
6
|
+
# component set. Lives under spec/runtime so it renders through the dummy app's
|
|
7
|
+
# real view stack (the engine's ui/ partials are on its view path because the
|
|
8
|
+
# engine is non-isolated).
|
|
9
|
+
#
|
|
10
|
+
# What it guards:
|
|
11
|
+
#
|
|
12
|
+
# - every component partial declares a `locals:` magic comment, so its public
|
|
13
|
+
# surface is explicit (the contract that makes the auto-wire + the override
|
|
14
|
+
# story honest);
|
|
15
|
+
# - a required strict local that is omitted RAISES rather than silently
|
|
16
|
+
# rendering nothing — the heart of the "strict locals" contract;
|
|
17
|
+
# - every seed component renders without error from its own preview.
|
|
18
|
+
#
|
|
19
|
+
# Rendering is done through ApplicationController so the auto-wired ui_* helpers
|
|
20
|
+
# (mixed into ActionController::Base by the engine) resolve inside the previews.
|
|
21
|
+
RSpec.describe "Design ui_* components", type: :integration do
|
|
22
|
+
# Render a partial the way a view would, with the design helpers available.
|
|
23
|
+
def render_partial(path, **locals)
|
|
24
|
+
ApplicationController.render(partial: path, locals: locals)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
describe "the strict-locals contract" do
|
|
28
|
+
it "every component partial declares a `locals:` magic comment" do
|
|
29
|
+
root = Design::Engine.root.join("app", "views", "ui")
|
|
30
|
+
partials = Dir[root.join("_*.html.erb")]
|
|
31
|
+
expect(partials).not_to be_empty
|
|
32
|
+
|
|
33
|
+
partials.each do |partial|
|
|
34
|
+
first_line = File.foreach(partial).first.to_s
|
|
35
|
+
expect(first_line).to match(/locals:/),
|
|
36
|
+
"#{File.basename(partial)} is missing its `# locals:` strict-locals header"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "raises when a required local is omitted (ui/tag needs label:)" do
|
|
41
|
+
# _tag.html.erb declares `locals: (label:, ...)` — label: is required, so
|
|
42
|
+
# rendering without it must raise rather than render an empty tag.
|
|
43
|
+
expect { render_partial("ui/tag") }.to raise_error(ArgumentError)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "renders ui/tag when the required local is supplied" do
|
|
47
|
+
html = render_partial("ui/tag", label: "Ready", status: :ready)
|
|
48
|
+
expect(html).to include("Ready")
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
describe "the seed components render from their previews" do
|
|
53
|
+
it "renders every public component's preview without error", :aggregate_failures do
|
|
54
|
+
Design.component_names.each do |name|
|
|
55
|
+
expect { render_partial("ui/previews/#{name}") }
|
|
56
|
+
.not_to raise_error, "ui/previews/_#{name} failed to render"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it "ships the button + tag seed components" do
|
|
61
|
+
expect(Design.component_names).to include("button", "tag")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it "renders ui_button with its block content" do
|
|
65
|
+
html = render_partial("ui/button", variant: :primary, content: "Save")
|
|
66
|
+
expect(html).to include("Save")
|
|
67
|
+
expect(html).to include("btn-primary")
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|