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.
Files changed (222) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +212 -12
  3. data/README.md +235 -82
  4. data/lib/generators/seams/accounts/accounts_generator.rb +31 -3
  5. data/lib/generators/seams/accounts/templates/app/controllers/memberships_controller.rb.tt +100 -0
  6. data/lib/generators/seams/accounts/templates/app/views/memberships/index.html.erb.tt +52 -0
  7. data/lib/generators/seams/accounts/templates/config/routes.rb.tt +10 -4
  8. data/lib/generators/seams/accounts/templates/lib/concerns/authorization.rb.tt +35 -2
  9. data/lib/generators/seams/accounts/templates/lib/engine.rb.tt +13 -0
  10. data/lib/generators/seams/accounts/templates/spec/runtime/authorization_spec.rb.tt +102 -0
  11. data/lib/generators/seams/accounts/templates/spec/runtime/memberships_flow_spec.rb.tt +145 -0
  12. data/lib/generators/seams/admin/admin_generator.rb +51 -4
  13. data/lib/generators/seams/admin/templates/README.md.tt +33 -3
  14. data/lib/generators/seams/admin/templates/app/controllers/admin/application_controller.rb.tt +156 -8
  15. data/lib/generators/seams/admin/templates/app/dashboards/admin/account_dashboard.rb.tt +9 -1
  16. data/lib/generators/seams/admin/templates/app/dashboards/admin/accounts_membership_dashboard.rb.tt +9 -1
  17. data/lib/generators/seams/admin/templates/app/dashboards/admin/identity_dashboard.rb.tt +6 -0
  18. data/lib/generators/seams/admin/templates/app/dashboards/admin/invitation_dashboard.rb.tt +7 -1
  19. data/lib/generators/seams/admin/templates/app/dashboards/admin/invoice_dashboard.rb.tt +6 -0
  20. data/lib/generators/seams/admin/templates/app/dashboards/admin/lifetime_pass_dashboard.rb.tt +6 -0
  21. data/lib/generators/seams/admin/templates/app/dashboards/admin/notification_dashboard.rb.tt +6 -0
  22. data/lib/generators/seams/admin/templates/app/dashboards/admin/notification_preference_dashboard.rb.tt +6 -0
  23. data/lib/generators/seams/admin/templates/app/dashboards/admin/plan_dashboard.rb.tt +6 -0
  24. data/lib/generators/seams/admin/templates/app/dashboards/admin/subscription_dashboard.rb.tt +6 -0
  25. data/lib/generators/seams/admin/templates/app/dashboards/admin/team_dashboard.rb.tt +12 -2
  26. data/lib/generators/seams/admin/templates/app/dashboards/admin/teams_membership_dashboard.rb.tt +7 -1
  27. data/lib/generators/seams/admin/templates/app/fields/admin/fields/belongs_to.rb.tt +12 -0
  28. data/lib/generators/seams/admin/templates/app/fields/admin/fields/dashboard_option.rb.tt +22 -0
  29. data/lib/generators/seams/admin/templates/app/fields/admin/fields/has_many.rb.tt +12 -0
  30. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/account_policy.rb.tt +4 -0
  31. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/accounts_membership_policy.rb.tt +6 -0
  32. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/application_policy.rb.tt +51 -10
  33. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/identity_policy.rb.tt +4 -0
  34. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/invitation_policy.rb.tt +6 -0
  35. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/invoice_policy.rb.tt +6 -0
  36. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/lifetime_pass_policy.rb.tt +6 -0
  37. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/notification_policy.rb.tt +6 -0
  38. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/notification_preference_policy.rb.tt +6 -0
  39. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/plan_policy.rb.tt +7 -0
  40. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/subscription_policy.rb.tt +6 -0
  41. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/team_policy.rb.tt +6 -0
  42. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/teams_membership_policy.rb.tt +6 -0
  43. data/lib/generators/seams/admin/templates/app/views/seams/admin/application/_index_header.html.erb.tt +57 -0
  44. data/lib/generators/seams/admin/templates/app/views/seams/admin/application/_navigation.html.erb.tt +14 -0
  45. data/lib/generators/seams/admin/templates/config/routes.rb.tt +24 -14
  46. data/lib/generators/seams/admin/templates/lib/concerns/authenticator.rb.tt +3 -0
  47. data/lib/generators/seams/admin/templates/lib/configuration.rb.tt +3 -1
  48. data/lib/generators/seams/admin/templates/spec/runtime/admin_boot_spec.rb.tt +4 -2
  49. data/lib/generators/seams/auth/add_oauth_provider/add_oauth_provider_generator.rb +1 -2
  50. data/lib/generators/seams/auth/auth_generator.rb +1 -2
  51. data/lib/generators/seams/auth/templates/app/controllers/oauth/callbacks_controller.rb.tt +3 -0
  52. data/lib/generators/seams/auth/templates/app/controllers/password_resets_controller.rb.tt +4 -0
  53. data/lib/generators/seams/auth/templates/app/controllers/registrations_controller.rb.tt +5 -0
  54. data/lib/generators/seams/auth/templates/app/controllers/sessions_controller.rb.tt +5 -0
  55. data/lib/generators/seams/auth/templates/db/migrate/create_auth_oauth_providers.rb.tt +5 -1
  56. data/lib/generators/seams/auth/templates/lib/concerns/authentication.rb.tt +12 -1
  57. data/lib/generators/seams/auth/templates/lib/engine.rb.tt +27 -0
  58. data/lib/generators/seams/billing/billing_generator.rb +4 -3
  59. data/lib/generators/seams/billing/templates/README.md.tt +26 -0
  60. data/lib/generators/seams/billing/templates/app/controllers/invoices_controller.rb.tt +2 -0
  61. data/lib/generators/seams/billing/templates/app/controllers/subscriptions_controller.rb.tt +2 -0
  62. data/lib/generators/seams/billing/templates/app/services/invoices/sync_service.rb.tt +8 -7
  63. data/lib/generators/seams/billing/templates/app/services/stripe_service.rb.tt +9 -7
  64. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_handler_base.rb.tt +18 -3
  65. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_handler_base.rb.tt +7 -21
  66. data/lib/generators/seams/billing/templates/lib/engine.rb.tt +15 -0
  67. data/lib/generators/seams/billing/templates/lib/gateways/stripe.rb.tt +10 -10
  68. data/lib/generators/seams/billing/templates/lib/stripe/client.rb.tt +40 -14
  69. data/lib/generators/seams/billing/templates/lib/stripe/payload.rb.tt +90 -0
  70. data/lib/generators/seams/billing/templates/lib/stripe/webhook_signature.rb.tt +11 -1
  71. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/charge_refunded.json.tt +1 -0
  72. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/checkout_session_completed.json.tt +1 -0
  73. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_created.json.tt +15 -4
  74. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_deleted.json.tt +7 -1
  75. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_trial_will_end.json.tt +18 -1
  76. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_updated.json.tt +26 -5
  77. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_created.json.tt +16 -2
  78. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_finalized.json.tt +16 -2
  79. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_paid.json.tt +17 -2
  80. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_payment_failed.json.tt +16 -2
  81. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_voided.json.tt +16 -2
  82. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/payment_intent_payment_failed.json.tt +1 -0
  83. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/payment_intent_succeeded.json.tt +1 -0
  84. data/lib/generators/seams/billing/templates/spec/gateways/stripe_spec.rb.tt +47 -1
  85. data/lib/generators/seams/billing/templates/spec/runtime/webhook_handlers_spec.rb.tt +39 -1
  86. data/lib/generators/seams/design/design_generator.rb +647 -0
  87. data/lib/generators/seams/design/templates/README.md.tt +129 -0
  88. data/lib/generators/seams/design/templates/app/assets/tailwind/_tokens.css +322 -0
  89. data/lib/generators/seams/design/templates/app/assets/tailwind/themes/_quire.css +42 -0
  90. data/lib/generators/seams/design/templates/app/controllers/design/dashboard_controller.rb.tt +30 -0
  91. data/lib/generators/seams/design/templates/app/controllers/design/guide_controller.rb.tt +28 -0
  92. data/lib/generators/seams/design/templates/app/form_builders/design/form_builder.rb.tt +90 -0
  93. data/lib/generators/seams/design/templates/app/helpers/design/ui_helper.rb.tt +37 -0
  94. data/lib/generators/seams/design/templates/app/views/design/dashboard/index.html.erb.tt +38 -0
  95. data/lib/generators/seams/design/templates/app/views/design/guide/index.html.erb.tt +19 -0
  96. data/lib/generators/seams/design/templates/app/views/layouts/application.html.erb.tt +61 -0
  97. data/lib/generators/seams/design/templates/app/views/layouts/design/guide.html.erb.tt +21 -0
  98. data/lib/generators/seams/design/templates/app/views/ui/_banner.html.erb.tt +10 -0
  99. data/lib/generators/seams/design/templates/app/views/ui/_breadcrumb.html.erb.tt +8 -0
  100. data/lib/generators/seams/design/templates/app/views/ui/_build_row.html.erb.tt +14 -0
  101. data/lib/generators/seams/design/templates/app/views/ui/_button.html.erb.tt +4 -0
  102. data/lib/generators/seams/design/templates/app/views/ui/_card.html.erb.tt +2 -0
  103. data/lib/generators/seams/design/templates/app/views/ui/_chapter_row.html.erb.tt +11 -0
  104. data/lib/generators/seams/design/templates/app/views/ui/_checkbox.html.erb.tt +12 -0
  105. data/lib/generators/seams/design/templates/app/views/ui/_counter.html.erb.tt +2 -0
  106. data/lib/generators/seams/design/templates/app/views/ui/_data_table.html.erb.tt +25 -0
  107. data/lib/generators/seams/design/templates/app/views/ui/_dialog.html.erb.tt +14 -0
  108. data/lib/generators/seams/design/templates/app/views/ui/_diff.html.erb.tt +12 -0
  109. data/lib/generators/seams/design/templates/app/views/ui/_drawer.html.erb.tt +7 -0
  110. data/lib/generators/seams/design/templates/app/views/ui/_empty.html.erb.tt +5 -0
  111. data/lib/generators/seams/design/templates/app/views/ui/_field.html.erb.tt +14 -0
  112. data/lib/generators/seams/design/templates/app/views/ui/_icon.html.erb.tt +2 -0
  113. data/lib/generators/seams/design/templates/app/views/ui/_icon_sprite.html.erb.tt +22 -0
  114. data/lib/generators/seams/design/templates/app/views/ui/_input_group.html.erb.tt +11 -0
  115. data/lib/generators/seams/design/templates/app/views/ui/_kbd.html.erb.tt +4 -0
  116. data/lib/generators/seams/design/templates/app/views/ui/_menu.html.erb.tt +18 -0
  117. data/lib/generators/seams/design/templates/app/views/ui/_meter.html.erb.tt +15 -0
  118. data/lib/generators/seams/design/templates/app/views/ui/_note.html.erb.tt +2 -0
  119. data/lib/generators/seams/design/templates/app/views/ui/_outline.html.erb.tt +9 -0
  120. data/lib/generators/seams/design/templates/app/views/ui/_pagination.html.erb.tt +12 -0
  121. data/lib/generators/seams/design/templates/app/views/ui/_panel.html.erb.tt +2 -0
  122. data/lib/generators/seams/design/templates/app/views/ui/_popover.html.erb.tt +2 -0
  123. data/lib/generators/seams/design/templates/app/views/ui/_radio.html.erb.tt +5 -0
  124. data/lib/generators/seams/design/templates/app/views/ui/_savestate.html.erb.tt +5 -0
  125. data/lib/generators/seams/design/templates/app/views/ui/_segmented.html.erb.tt +7 -0
  126. data/lib/generators/seams/design/templates/app/views/ui/_stepper.html.erb.tt +12 -0
  127. data/lib/generators/seams/design/templates/app/views/ui/_switch.html.erb.tt +6 -0
  128. data/lib/generators/seams/design/templates/app/views/ui/_tag.html.erb.tt +2 -0
  129. data/lib/generators/seams/design/templates/app/views/ui/_toast.html.erb.tt +12 -0
  130. data/lib/generators/seams/design/templates/app/views/ui/_toolbar.html.erb.tt +15 -0
  131. data/lib/generators/seams/design/templates/app/views/ui/previews/_banner.html.erb.tt +8 -0
  132. data/lib/generators/seams/design/templates/app/views/ui/previews/_breadcrumb.html.erb.tt +5 -0
  133. data/lib/generators/seams/design/templates/app/views/ui/previews/_build_row.html.erb.tt +6 -0
  134. data/lib/generators/seams/design/templates/app/views/ui/previews/_button.html.erb.tt +9 -0
  135. data/lib/generators/seams/design/templates/app/views/ui/previews/_card.html.erb.tt +12 -0
  136. data/lib/generators/seams/design/templates/app/views/ui/previews/_chapter_row.html.erb.tt +6 -0
  137. data/lib/generators/seams/design/templates/app/views/ui/previews/_checkbox.html.erb.tt +6 -0
  138. data/lib/generators/seams/design/templates/app/views/ui/previews/_counter.html.erb.tt +6 -0
  139. data/lib/generators/seams/design/templates/app/views/ui/previews/_data_table.html.erb.tt +14 -0
  140. data/lib/generators/seams/design/templates/app/views/ui/previews/_dialog.html.erb.tt +8 -0
  141. data/lib/generators/seams/design/templates/app/views/ui/previews/_diff.html.erb.tt +10 -0
  142. data/lib/generators/seams/design/templates/app/views/ui/previews/_drawer.html.erb.tt +5 -0
  143. data/lib/generators/seams/design/templates/app/views/ui/previews/_empty.html.erb.tt +5 -0
  144. data/lib/generators/seams/design/templates/app/views/ui/previews/_field.html.erb.tt +5 -0
  145. data/lib/generators/seams/design/templates/app/views/ui/previews/_input_group.html.erb.tt +5 -0
  146. data/lib/generators/seams/design/templates/app/views/ui/previews/_kbd.html.erb.tt +5 -0
  147. data/lib/generators/seams/design/templates/app/views/ui/previews/_menu.html.erb.tt +9 -0
  148. data/lib/generators/seams/design/templates/app/views/ui/previews/_meter.html.erb.tt +6 -0
  149. data/lib/generators/seams/design/templates/app/views/ui/previews/_note.html.erb.tt +2 -0
  150. data/lib/generators/seams/design/templates/app/views/ui/previews/_outline.html.erb.tt +8 -0
  151. data/lib/generators/seams/design/templates/app/views/ui/previews/_pagination.html.erb.tt +5 -0
  152. data/lib/generators/seams/design/templates/app/views/ui/previews/_panel.html.erb.tt +5 -0
  153. data/lib/generators/seams/design/templates/app/views/ui/previews/_popover.html.erb.tt +2 -0
  154. data/lib/generators/seams/design/templates/app/views/ui/previews/_radio.html.erb.tt +6 -0
  155. data/lib/generators/seams/design/templates/app/views/ui/previews/_savestate.html.erb.tt +4 -0
  156. data/lib/generators/seams/design/templates/app/views/ui/previews/_segmented.html.erb.tt +15 -0
  157. data/lib/generators/seams/design/templates/app/views/ui/previews/_stepper.html.erb.tt +6 -0
  158. data/lib/generators/seams/design/templates/app/views/ui/previews/_switch.html.erb.tt +5 -0
  159. data/lib/generators/seams/design/templates/app/views/ui/previews/_tag.html.erb.tt +8 -0
  160. data/lib/generators/seams/design/templates/app/views/ui/previews/_toast.html.erb.tt +6 -0
  161. data/lib/generators/seams/design/templates/app/views/ui/previews/_toolbar.html.erb.tt +11 -0
  162. data/lib/generators/seams/design/templates/lib/design/components.rb.tt +22 -0
  163. data/lib/generators/seams/design/templates/lib/design.rb.tt +18 -0
  164. data/lib/generators/seams/design/templates/lib/engine.rb.tt +38 -0
  165. data/lib/generators/seams/design/templates/lib/generators/design/component/component_generator.rb.tt +52 -0
  166. data/lib/generators/seams/design/templates/lib/generators/design/component/templates/component.html.erb.tt +5 -0
  167. data/lib/generators/seams/design/templates/lib/generators/design/component/templates/preview.html.erb.tt +3 -0
  168. data/lib/generators/seams/design/templates/spec/runtime/design_boot_spec.rb.tt +79 -0
  169. data/lib/generators/seams/design/templates/spec/runtime/form_builder_spec.rb.tt +97 -0
  170. data/lib/generators/seams/design/templates/spec/runtime/guide_spec.rb.tt +40 -0
  171. data/lib/generators/seams/design/templates/spec/runtime/ui_components_spec.rb.tt +70 -0
  172. data/lib/generators/seams/engine/engine_generator.rb +6 -0
  173. data/lib/generators/seams/engine/templates/Gemfile.tt +1 -1
  174. data/lib/generators/seams/engine/templates/app/application_controller.rb.tt +6 -0
  175. data/lib/generators/seams/engine/templates/rubocop.yml.tt +9 -0
  176. data/lib/generators/seams/install/install_generator.rb +72 -6
  177. data/lib/generators/seams/install/templates/Dockerfile.tt +2 -2
  178. data/lib/generators/seams/install/templates/bin_seams.tt +7 -4
  179. data/lib/generators/seams/install/templates/ci.yml.tt +18 -8
  180. data/lib/generators/seams/install/templates/deploy.yml.tt +4 -2
  181. data/lib/generators/seams/install/templates/doc/ARCHITECTURE.md.tt +1 -1
  182. data/lib/generators/seams/install/templates/docker-entrypoint.tt +4 -2
  183. data/lib/generators/seams/install/templates/herb.yml.tt +10 -0
  184. data/lib/generators/seams/install/templates/lefthook.yml.tt +25 -0
  185. data/lib/generators/seams/install/templates/ruby-version.tt +1 -1
  186. data/lib/generators/seams/install/templates/seams.rake.tt +13 -1
  187. data/lib/generators/seams/install/templates/strong_migrations.rb.tt +26 -0
  188. data/lib/generators/seams/notifications/notifications_generator.rb +3 -2
  189. data/lib/generators/seams/notifications/templates/app/controllers/preferences_controller.rb.tt +5 -13
  190. data/lib/generators/seams/notifications/templates/lib/engine.rb.tt +12 -0
  191. data/lib/generators/seams/notifications/templates/lib/notifications.rb.tt +1 -0
  192. data/lib/generators/seams/notifications/templates/lib/preferences.rb.tt +36 -0
  193. data/lib/generators/seams/permissions/permissions_generator.rb +97 -0
  194. data/lib/generators/seams/permissions/templates/config/initializers/seams_permissions.rb.tt +35 -0
  195. data/lib/generators/seams/teams/templates/app/controllers/invitations_controller.rb.tt +27 -11
  196. data/lib/generators/seams/teams/templates/app/controllers/teams_controller.rb.tt +5 -13
  197. data/lib/generators/seams/teams/templates/lib/concerns/authorization.rb.tt +2 -4
  198. data/lib/generators/seams/teams/templates/lib/engine.rb.tt +13 -0
  199. data/lib/seams/cli/list.rb +15 -0
  200. data/lib/seams/cli/quality.rb +46 -5
  201. data/lib/seams/cli/resolve.rb +9 -5
  202. data/lib/seams/cli/test_changed.rb +5 -1
  203. data/lib/seams/cli.rb +20 -0
  204. data/lib/seams/configuration.rb +24 -1
  205. data/lib/seams/cops/no_cross_engine_dependency.rb +39 -7
  206. data/lib/seams/cops/no_cross_engine_model_access.rb +79 -1
  207. data/lib/seams/event_registry.rb +19 -0
  208. data/lib/seams/events/adapter.rb +9 -0
  209. data/lib/seams/events/adapters/active_support.rb +4 -0
  210. data/lib/seams/events/publisher.rb +29 -0
  211. data/lib/seams/events.rb +1 -0
  212. data/lib/seams/generators/dummy_app_writer.rb +21 -0
  213. data/lib/seams/generators/follow_up_generator.rb +1 -2
  214. data/lib/seams/generators/host_injector.rb +9 -2
  215. data/lib/seams/generators/splicer.rb +2 -2
  216. data/lib/seams/observability/adapter.rb +22 -0
  217. data/lib/seams/observability.rb +6 -0
  218. data/lib/seams/permission_registry.rb +66 -0
  219. data/lib/seams/permissions.rb +120 -0
  220. data/lib/seams/version.rb +1 -1
  221. data/lib/seams.rb +7 -0
  222. metadata +107 -3
@@ -0,0 +1,6 @@
1
+ <%%# locals: () -%>
2
+ <div class="row-wrap">
3
+ <%%= ui_counter(current: 2140, max: 4000) %>
4
+ <%%= ui_counter(current: 3902, max: 4000) %>
5
+ <%%= ui_counter(current: 4117, max: 4000) %>
6
+ </div>
@@ -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,10 @@
1
+ <%%# locals: () -%>
2
+ <%%= ui_diff(
3
+ file: "config/application.rb",
4
+ lines: [
5
+ [:ctx, "config.load_defaults 8.0"],
6
+ [:del, "config.time_zone = \"UTC\""],
7
+ [:add, "config.time_zone = \"London\""],
8
+ [:ctx, "config.eager_load = true"]
9
+ ]
10
+ ) %>
@@ -0,0 +1,5 @@
1
+ <%%# locals: () -%>
2
+ <%%= ui_drawer(title: "History") do %>
3
+ <p class="small muted mb-3">Every saved revision, newest first.</p>
4
+ <p class="small">You can restore any version or compare it with the current draft.</p>
5
+ <%% end %>
@@ -0,0 +1,5 @@
1
+ <%%# locals: () -%>
2
+ <%%= ui_empty(title: "Nothing here yet") do %>
3
+ <p class="muted">There's nothing to show right now. Create your first item to get started.</p>
4
+ <%%= ui_button(variant: :primary) { "Create item" } %>
5
+ <%% end %>
@@ -0,0 +1,5 @@
1
+ <%%# locals: () -%>
2
+ <div class="grid grid-2">
3
+ <%%= ui_field(label: "Project name", name: "name", value: "Acme", hint: "Shown across the app.") %>
4
+ <%%= ui_field(label: "Slug", name: "slug", value: "ac", error: "A slug needs at least 3 characters.") %>
5
+ </div>
@@ -0,0 +1,5 @@
1
+ <%%# locals: () -%>
2
+ <div class="grid grid-2">
3
+ <%%= ui_input_group(name: "price", value: "9.99", prefix: "$", suffix: "/mo") %>
4
+ <%%= ui_input_group(name: "subdomain", value: "acme", suffix: ".example.com", placeholder: "your-team") %>
5
+ </div>
@@ -0,0 +1,5 @@
1
+ <%%# locals: () -%>
2
+ <div class="row-wrap">
3
+ <%%= ui_kbd(keys: ["⌘", "K"]) %>
4
+ <%%= ui_kbd(keys: ["g", "then", "w"]) %>
5
+ </div>
@@ -0,0 +1,9 @@
1
+ <%%# locals: () -%>
2
+ <%%= ui_menu(
3
+ label: "Account",
4
+ items: [
5
+ { label: "Profile", href: "/account/profile" },
6
+ { label: "Billing", href: "/account/billing" },
7
+ { label: "Sign out", danger: true, separator_before: true }
8
+ ]
9
+ ) %>
@@ -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,2 @@
1
+ <%%# locals: () -%>
2
+ <%%= ui_note do %>Use a note to call out a useful aside or a piece of context that supports the main content without interrupting it.<%% end %>
@@ -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,5 @@
1
+ <%%# locals: () -%>
2
+ <div class="stack">
3
+ <%%= ui_pagination(current: 1, total: 5) %>
4
+ <%%= ui_pagination(current: 3, total: 5) %>
5
+ </div>
@@ -0,0 +1,5 @@
1
+ <%%# locals: () -%>
2
+ <%%= ui_panel do %>
3
+ <p class="label">Summary</p>
4
+ <p class="mt-2">A panel groups related content on a raised surface. Use it to set a region apart from the page without the full weight of a card.</p>
5
+ <%% end %>
@@ -0,0 +1,2 @@
1
+ <%%# locals: () -%>
2
+ <%%= ui_popover(content: "Changes save automatically as you type.") %>
@@ -0,0 +1,6 @@
1
+ <%%# locals: () -%>
2
+ <fieldset class="fieldset">
3
+ <legend>Default role</legend>
4
+ <%%= ui_radio(label: "Member", name: "role", value: "member", checked: true) %>
5
+ <%%= ui_radio(label: "Admin", name: "role", value: "admin") %>
6
+ </fieldset>
@@ -0,0 +1,4 @@
1
+ <%%# locals: () -%>
2
+ <%%= ui_savestate(state: :saved) %>
3
+ <%%= ui_savestate(state: :saving) %>
4
+ <%%= ui_savestate(state: :unsaved) %>
@@ -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,5 @@
1
+ <%%# locals: () -%>
2
+ <div class="stack">
3
+ <%%= ui_switch(label: "Make this project private", name: "private") %>
4
+ <%%= ui_switch(label: "Email me on activity", name: "activity_email", checked: true) %>
5
+ </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,11 @@
1
+ <%%# locals: () -%>
2
+ <%%= ui_toolbar(
3
+ label: "Text formatting",
4
+ items: [
5
+ { label: "Heading" },
6
+ { label: "Bold" },
7
+ { label: "Italic" },
8
+ { label: "Link", icon: :edit, sep_before: true },
9
+ { label: "Preview", icon: :eye }
10
+ ]
11
+ ) %>
@@ -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
@@ -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,3 @@
1
+ <%%# locals: () -%>
2
+ <%%# TODO: show every state of <%= file_name %>. -%>
3
+ <%%= ui_<%= file_name %> { "<%= file_name.humanize %>" } %>
@@ -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