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,129 @@
1
+ # Design
2
+
3
+ The seams design system, as a callable component library — a faithful
4
+ extraction of quire-saas's proven "Compositor". Strict-locals ERB partials,
5
+ auto-wired `ui_*` helpers, a `Design::FormBuilder`, and a Tailwind v4 `@theme`
6
+ token layer. Generated by `bin/seams design`.
7
+
8
+ ## What it is
9
+
10
+ A component is a strict-locals ERB partial under `app/views/ui/` with a
11
+ companion preview under `app/views/ui/previews/`. The preview is what makes a
12
+ component "public": it gives the auto-wired `ui_<name>` helper and the living
13
+ guide their list.
14
+
15
+ ```erb
16
+ <%%= ui_button(variant: :primary) { "Save" } %>
17
+ <%%= ui_tag(label: "Built", status: :ready) %>
18
+
19
+ <%%= form_with model: @record do |f| %>
20
+ <%%= f.ui_text_field :title, label: "Title", hint: "Shown on the cover" %>
21
+ <%% end %>
22
+ ```
23
+
24
+ ## Living gallery (dev/test only)
25
+
26
+ `/design/guide` renders every component from its real preview, so the docs
27
+ cannot drift from the code. Add a component (a partial + a preview under
28
+ `app/views/ui/`) and it appears in the gallery — with its `ui_<name>` helper —
29
+ automatically. The route is dev/test-only: it is drawn inside an
30
+ `if Rails.env.local?` guard in the host routes, and the controller 404s outside
31
+ `Rails.env.local?`.
32
+
33
+ ## Non-isolated by design
34
+
35
+ Unlike every other canonical seams engine, the design engine is **not**
36
+ `isolate_namespace`d. Its partials live in the host's view paths and its
37
+ `Design::UiHelper` is mixed into `ActionController::Base`, so a component renders
38
+ anywhere — in the host's views and in every other engine's views — without
39
+ ceremony. Ruby constants live under `Design::`; the view + helper surface is
40
+ `ui`.
41
+
42
+ ## Tailwind v4 is a hard dependency
43
+
44
+ The token layer is Tailwind-native `@theme`. The generator injects
45
+ `tailwindcss-rails` and writes the neutral default tokens into the host's
46
+ `app/assets/tailwind/application.css`. **Retheme by overriding those tokens.**
47
+ Nothing else changes.
48
+
49
+ ```css
50
+ @theme {
51
+ --color-paper: …; --color-ink: …; --color-accent: …;
52
+ --font-display: …; --font-sans: …;
53
+ }
54
+ ```
55
+
56
+ ## Host wiring (done by the generator)
57
+
58
+ - `tailwindcss-rails` injected into the host Gemfile.
59
+ - The neutral `@theme` token block written into the host
60
+ `app/assets/tailwind/application.css`.
61
+ - `Design::FormBuilder` set as the host's `default_form_builder`, so every
62
+ `form_with` / `form_for` gets the `f.ui_*` field methods (the standard
63
+ `f.text_field` / `f.select` / `f.submit` are untouched).
64
+ - The icon sprite (`<%%= render "ui/icon_sprite" %>`) rendered once near the top
65
+ of `<body>` in the host layout.
66
+ - The dev/test-only living-gallery route (`/design/guide`) drawn into the host
67
+ routes, inside an `if Rails.env.local?` guard.
68
+
69
+ ## Generate
70
+
71
+ ```
72
+ bin/seams design # the component system (tokens, ui_* components, gallery, FormBuilder)
73
+ bin/seams design --shell # the above PLUS an app layout + starter dashboard + route
74
+ ```
75
+
76
+ `--shell` is opt-in. Without it, no host layout or dashboard is generated — the
77
+ host keeps its own. With it, the generator writes a default
78
+ `app/views/layouts/application.html.erb` (header, nav, flash banners, footer —
79
+ all built from `ui_*` components) and a starter signed-in dashboard
80
+ (`Design::DashboardController`) wired to `root` + `/dashboard`, so the host boots
81
+ looking like a product.
82
+
83
+ ## Extend — add a component
84
+
85
+ ```
86
+ rails g design:component badge
87
+ ```
88
+
89
+ This writes `app/views/ui/_badge.html.erb` (a strict-locals partial) and
90
+ `app/views/ui/previews/_badge.html.erb`. The preview is what makes it public:
91
+ `ui_badge` becomes callable and the component appears in `/design/guide`
92
+ automatically — no registration.
93
+
94
+ ## Override — eject a component
95
+
96
+ Take ownership of a shipped component (to change its markup or copy) without
97
+ forking the engine:
98
+
99
+ ```
100
+ bin/seams resolve --eject design/app/views/ui/_button.html.erb
101
+ ```
102
+
103
+ This stamps the host file with an eject header. The generator then skips it on
104
+ every future `bin/seams design`, so your version survives regenerates. The same
105
+ works for the `--shell` layout and dashboard.
106
+
107
+ ## Retheme — override the tokens
108
+
109
+ The `@theme` block in `app/assets/tailwind/application.css` is the single source
110
+ every component reads (via the short `:root` aliases). Override those tokens and
111
+ the whole app reskins — no component, markup, or helper changes.
112
+
113
+ The generator ships a worked example, the **quire** theme (warm paper / garnet /
114
+ Spectral), at `app/assets/tailwind/themes/_quire.css`. Apply it with one line in
115
+ `application.css`, after the `seams:design tokens` block:
116
+
117
+ ```css
118
+ @import "themes/quire";
119
+ ```
120
+
121
+ Delete the line to return to the neutral default. See
122
+ `doc/design-system/DESIGN_SYSTEM_THEMING.md` for the full token surface and how to author your
123
+ own theme.
124
+
125
+ ## Run the engine specs
126
+
127
+ ```
128
+ bin/rails seams:test[design]
129
+ ```
@@ -0,0 +1,322 @@
1
+ /* ---------------------------------------------------------------------------
2
+ seams:design tokens — the neutral default design tokens, owned by Tailwind @theme.
3
+ This is the SINGLE SOURCE every ui_* component reads. Retheme the whole system
4
+ by overriding these values; nothing else changes. (seams design)
5
+
6
+ One strong, neutral default: refined paper/ink surfaces, a single restrained
7
+ indigo accent, and a clean type pairing. Every text token below is WCAG-AA on
8
+ the surfaces it sits on (faint is decorative-only / AA-large).
9
+
10
+ Type: Inter is named first but is NOT self-hosted — it degrades gracefully to
11
+ the system sans stack, so the default boots with zero web-font requests. Host
12
+ Inter (or any pairing) by adding it to the bundle and it takes over; the token
13
+ stays the single switch. --------------------------------------------------- */
14
+ @theme {
15
+ --color-paper:#ffffff; --color-paper-raised:#f5f6f8; --color-card:#ffffff;
16
+ --color-ink:#15171c; --color-ink-2:#3a3f48; --color-muted:#5c636f; --color-faint:#767d8a;
17
+ --color-line:#e6e8ec; --color-line-2:#d2d6dd;
18
+ --color-accent:#2456c7; --color-accent-deep:#1b3f96;
19
+ --color-ready:#0f6b41; --color-ready-bg:#e7f3ec; --color-ready-line:#a7d3bd;
20
+ --color-progress:#8a5208; --color-progress-bg:#fbf1e0; --color-progress-line:#eccf97;
21
+ --color-alert:#b81d31; --color-alert-bg:#fbecee; --color-alert-line:#f1b6bd;
22
+ --font-display:"Inter",ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
23
+ --font-sans:"Inter",ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
24
+ --font-mono:ui-monospace,"SF Mono",SFMono-Regular,Menlo,Consolas,monospace;
25
+ }
26
+
27
+ /* Short token aliases the component layer references (value lives once, in @theme).
28
+ Components read `var(--ink)`, `var(--accent)`, `var(--s-4)` …; theming the
29
+ system means overriding the @theme block above, never these aliases. */
30
+ :root{
31
+ --paper:var(--color-paper); --paper-raised:var(--color-paper-raised); --card:var(--color-card);
32
+ --ink:var(--color-ink); --ink-2:var(--color-ink-2); --muted:var(--color-muted); --faint:var(--color-faint);
33
+ --line:var(--color-line); --line-2:var(--color-line-2);
34
+ --accent:var(--color-accent); --accent-deep:var(--color-accent-deep);
35
+ --ready:var(--color-ready); --ready-bg:var(--color-ready-bg); --ready-line:var(--color-ready-line);
36
+ --progress:var(--color-progress); --progress-bg:var(--color-progress-bg); --progress-line:var(--color-progress-line);
37
+ --alert:var(--color-alert); --alert-bg:var(--color-alert-bg); --alert-line:var(--color-alert-line);
38
+ --focus:var(--color-accent);
39
+ --display:var(--font-display); --sans:var(--font-sans); --mono:var(--font-mono);
40
+ --t-display-xl:44px; --t-display:34px; --t-h1:27px; --t-h2:21px; --t-h3:17px; --t-body:16px; --t-sm:14px; --t-cap:12.5px;
41
+ --s-1:4px; --s-2:8px; --s-3:12px; --s-4:16px; --s-5:24px; --s-6:32px; --s-7:48px; --s-8:64px; --s-9:96px;
42
+ --r-sm:5px; --r:9px; --r-lg:14px; --r-pill:999px;
43
+ --shadow-sm:0 1px 2px rgba(21,23,28,.05); --shadow:0 6px 20px -10px rgba(21,23,28,.18); --shadow-pop:0 28px 64px -24px rgba(21,23,28,.40);
44
+ --ease:cubic-bezier(.2,.6,.2,1); --fast:120ms; --med:190ms;
45
+ --measure:68ch; --shell-max:1180px;
46
+ --z-base:0; --z-raised:10; --z-sticky:40; --z-popover:70; --z-overlay:80; --z-toast:95;
47
+ --bp-sm:680px; --bp-md:860px; --bp-lg:1080px;
48
+ }
49
+
50
+ /* Base bits Tailwind preflight doesn't cover (preflight owns the rest). */
51
+ @layer base{
52
+ ::selection{background:var(--ink);color:var(--paper)}
53
+ :where(a,button,input,textarea,select,summary,[tabindex]):focus-visible{outline:2px solid var(--focus);outline-offset:2px;border-radius:var(--r-sm)}
54
+ .skip{position:absolute;left:-9999px;top:var(--s-2);z-index:80;background:var(--ink);color:var(--paper);padding:var(--s-2) var(--s-4);border-radius:var(--r);font-weight:600}
55
+ .skip:focus{left:var(--s-2)}
56
+ }
57
+
58
+ /* ---------------------------------------------------------------------------
59
+ The ui_* component layer — the visual half of the single source.
60
+
61
+ Every `ui_<name>` partial renders these classes (`.btn`, `.card`, `.field`,
62
+ `.tag` …). Like the tokens above, this layer is theme-agnostic: it reads ONLY
63
+ the short aliases (`var(--ink)`, `var(--accent)`, `var(--s-4)` …), so
64
+ overriding the @theme block at the top reskins every component — no rule below
65
+ names a literal colour or font. This is what makes "retheme = token override"
66
+ true (see the example theme + theming guide). It is a neutral extraction of
67
+ quire-saas's proven Compositor component CSS, with every brand-specific
68
+ colour and font replaced by a token alias. -------------------------------- */
69
+ @layer components{
70
+ /* Type roles */
71
+ .display-xl{font-family:var(--display);font-size:var(--t-display-xl);line-height:1.06;letter-spacing:-.02em;font-weight:600}
72
+ .display{font-family:var(--display);font-size:var(--t-display);line-height:1.12;letter-spacing:-.015em;font-weight:600}
73
+ .h1{font-family:var(--display);font-size:var(--t-h1);line-height:1.2;font-weight:600}
74
+ .h2{font-size:var(--t-h2);line-height:1.3;font-weight:600;letter-spacing:-.01em}
75
+ .h3{font-size:var(--t-h3);line-height:1.4;font-weight:600}
76
+ .lede{font-size:19px;line-height:1.55;color:var(--ink-2);max-width:var(--measure);font-weight:400}
77
+ .prose{max-width:var(--measure)}
78
+ .prose p+p{margin-top:var(--s-4)}
79
+ .small{font-size:var(--t-sm)}
80
+ .muted{color:var(--muted)}
81
+ .mono{font-family:var(--mono)}
82
+ .tnum{font-variant-numeric:tabular-nums}
83
+ .eyebrow{font-family:var(--mono);font-size:var(--t-cap);letter-spacing:.14em;
84
+ text-transform:uppercase;color:var(--muted);display:inline-flex;gap:var(--s-2);align-items:center}
85
+ .eyebrow::before{content:"";width:18px;height:1px;background:var(--line-2)}
86
+
87
+ /* Layout primitives */
88
+ .shell{max-width:var(--shell-max);margin:0 auto;padding:0 var(--s-5)}
89
+ .stack>*+*{margin-top:var(--s-5)}
90
+ .stack-lg>*+*{margin-top:var(--s-7)}
91
+ .row{display:flex;gap:var(--s-3);align-items:center}
92
+ .row-wrap{display:flex;gap:var(--s-3);align-items:center;flex-wrap:wrap}
93
+ .spacer{flex:1}
94
+ .grid{display:grid;gap:var(--s-4)}
95
+ .grid-2{grid-template-columns:repeat(2,minmax(0,1fr))}
96
+ .grid-3{grid-template-columns:repeat(3,minmax(0,1fr))}
97
+ .grid-auto{grid-template-columns:repeat(auto-fit,minmax(220px,1fr))}
98
+ .section{padding:var(--s-8) 0}
99
+ .center{text-align:center}
100
+ .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
101
+ @media(max-width:860px){.grid-2,.grid-3{grid-template-columns:1fr}}
102
+ @media(max-width:680px){.grid{grid-template-columns:1fr !important}}
103
+
104
+ /* Keycaps */
105
+ .kbd{font-family:var(--mono);font-size:11px;line-height:18px;min-width:18px;text-align:center;display:inline-block;
106
+ background:var(--paper-raised);border:1px solid var(--line-2);border-bottom-width:2px;
107
+ border-radius:var(--r-sm);padding:0 5px;color:var(--ink-2)}
108
+
109
+ /* Buttons */
110
+ .btn{display:inline-flex;align-items:center;gap:var(--s-2);font-family:var(--sans);
111
+ font-size:var(--t-sm);font-weight:600;line-height:1;border:1px solid var(--line-2);
112
+ background:var(--card);color:var(--ink);border-radius:var(--r);padding:9px 14px;cursor:pointer;text-decoration:none;
113
+ transition:border-color var(--fast) var(--ease),background var(--fast) var(--ease)}
114
+ .btn:hover{border-color:var(--ink)}
115
+ .btn-primary{background:var(--ink);border-color:var(--ink);color:var(--paper)}
116
+ .btn-primary:hover{background:#000}
117
+ .btn-accent{background:var(--accent);border-color:var(--accent);color:#fff}
118
+ .btn-accent:hover{background:var(--accent-deep);border-color:var(--accent-deep)}
119
+ .btn-ghost{border-color:transparent;background:transparent}
120
+ .btn-ghost:hover{background:var(--paper-raised);border-color:transparent}
121
+ .btn-danger{color:var(--alert);border-color:var(--alert-line)}
122
+ .btn-danger:hover{background:var(--alert-bg);border-color:var(--alert)}
123
+ .btn-sm{padding:6px 10px;font-size:var(--t-cap)}
124
+ .btn[disabled],.btn[aria-disabled="true"]{opacity:.5;cursor:not-allowed}
125
+
126
+ /* Surfaces: card / panel / note */
127
+ .card{background:var(--card);border:1px solid var(--line);border-radius:var(--r-lg);padding:var(--s-5)}
128
+ .panel{background:var(--paper-raised);border:1px solid var(--line);border-radius:var(--r-lg);padding:var(--s-5)}
129
+ .note{background:var(--paper-raised);border-left:3px solid var(--accent);
130
+ padding:var(--s-3) var(--s-4);border-radius:0 var(--r) var(--r) 0;font-size:var(--t-sm);color:var(--ink-2)}
131
+
132
+ /* Tags / status pills */
133
+ .tag{display:inline-flex;align-items:center;gap:6px;font-size:var(--t-cap);font-weight:600;
134
+ border-radius:var(--r-pill);padding:2px 9px;border:1px solid transparent}
135
+ .tag::before{content:"";width:6px;height:6px;border-radius:var(--r-pill);background:currentColor}
136
+ .tag-ready{background:var(--ready-bg);color:var(--ready);border-color:var(--ready-line)}
137
+ .tag-progress{background:var(--progress-bg);color:var(--progress);border-color:var(--progress-line)}
138
+ .tag-alert{background:var(--alert-bg);color:var(--alert);border-color:var(--alert-line)}
139
+ .tag-neutral{background:var(--paper-raised);color:var(--ink-2);border-color:var(--line-2)}
140
+ .tag-plain::before{display:none}
141
+
142
+ /* Forms */
143
+ .field{display:block}
144
+ .field>.label{display:block;font-size:var(--t-sm);font-weight:600;margin-bottom:6px}
145
+ .field>.hint{font-size:var(--t-cap);color:var(--muted);margin-top:6px}
146
+ .input,.textarea,.select{width:100%;font-family:var(--sans);font-size:var(--t-sm);color:var(--ink);
147
+ background:var(--card);border:1px solid var(--line-2);border-radius:var(--r);padding:9px 11px;line-height:1.4}
148
+ .input::placeholder,.textarea::placeholder{color:var(--faint)}
149
+ .input:focus,.textarea:focus,.select:focus{outline:2px solid var(--focus);outline-offset:1px;border-color:var(--ink)}
150
+ .textarea{min-height:96px;resize:vertical}
151
+ .field-error .input,.field-error .textarea{border-color:var(--alert)}
152
+ .field-error>.hint{color:var(--alert)}
153
+ .choice{display:flex;gap:10px;align-items:flex-start;padding:6px 0;cursor:pointer;font-size:var(--t-sm)}
154
+ .choice input[type=checkbox],.choice input[type=radio]{appearance:auto;accent-color:var(--ink);
155
+ width:17px;height:17px;margin:1px 0 0;flex:none;cursor:pointer}
156
+ .choice .ct{display:block}
157
+ .choice .cd{display:block;color:var(--muted);font-size:var(--t-cap);margin-top:2px}
158
+ .switch{display:inline-flex;align-items:center;gap:10px;cursor:pointer;font-size:var(--t-sm)}
159
+ .switch input{position:absolute;opacity:0;width:0;height:0}
160
+ .switch .track{width:38px;height:22px;border-radius:var(--r-pill);background:var(--line-2);
161
+ position:relative;transition:background var(--fast) var(--ease);flex:none}
162
+ .switch .track::after{content:"";position:absolute;top:2px;left:2px;width:18px;height:18px;border-radius:var(--r-pill);
163
+ background:var(--card);box-shadow:var(--shadow-sm);transition:transform var(--fast) var(--ease)}
164
+ .switch input:checked+.track{background:var(--ready)}
165
+ .switch input:checked+.track::after{transform:translateX(16px)}
166
+ .switch input:focus-visible+.track{outline:2px solid var(--focus);outline-offset:2px}
167
+ .input-group{display:flex;align-items:stretch;border:1px solid var(--line-2);border-radius:var(--r);overflow:hidden;background:var(--card)}
168
+ .input-group:focus-within{outline:2px solid var(--focus);outline-offset:1px;border-color:var(--ink)}
169
+ .input-group .affix{display:flex;align-items:center;padding:0 11px;background:var(--paper-raised);
170
+ color:var(--muted);font-size:var(--t-sm);border-right:1px solid var(--line)}
171
+ .input-group .affix.suffix{border-right:0;border-left:1px solid var(--line)}
172
+ .input-group .input{border:0;border-radius:0;outline:0}
173
+ .counter{font-family:var(--mono);font-size:var(--t-cap);color:var(--muted)}
174
+ .counter.warn{color:var(--progress)}
175
+ .counter.over{color:var(--alert)}
176
+
177
+ /* Tables */
178
+ .table-wrap{border:1px solid var(--line);border-radius:var(--r-lg);overflow:auto}
179
+ .table{width:100%;border-collapse:collapse;font-size:var(--t-sm)}
180
+ .table th,.table td{text-align:left;padding:11px 14px;border-bottom:1px solid var(--line);vertical-align:top;overflow-wrap:break-word}
181
+ .table th{font-size:var(--t-cap);letter-spacing:.04em;text-transform:uppercase;color:var(--muted);font-weight:600}
182
+ .table tr:hover td{background:var(--paper-raised)}
183
+ .table .num{font-family:var(--mono);font-variant-numeric:tabular-nums;text-align:right}
184
+ .table th.sortable{cursor:pointer;user-select:none}
185
+ .table th.sortable .ar{color:var(--faint);margin-left:4px;font-size:10px}
186
+ .table th[aria-sort="ascending"] .ar,.table th[aria-sort="descending"] .ar{color:var(--ink)}
187
+
188
+ /* Empty state */
189
+ .empty{text-align:center;padding:var(--s-8) var(--s-5);border:1px dashed var(--line-2);border-radius:var(--r-lg);background:var(--card)}
190
+ .empty .h3{margin-bottom:6px}
191
+ .empty p{color:var(--muted);max-width:46ch;margin:0 auto var(--s-4)}
192
+
193
+ /* Icons */
194
+ .ic{width:20px;height:20px;display:inline-block;vertical-align:-.18em;flex:none;
195
+ fill:none;stroke:currentColor;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round}
196
+ .ic-sm{width:16px;height:16px}
197
+ .ic-lg{width:24px;height:24px}
198
+
199
+ /* Navigation: breadcrumb / pagination / menu / segmented / stepper / toolbar / outline */
200
+ .crumbs{display:flex;align-items:center;gap:6px;font-size:var(--t-sm);color:var(--muted);min-width:0;list-style:none;margin:0;padding:0}
201
+ .crumbs a{color:var(--muted);text-decoration:none;white-space:nowrap}
202
+ .crumbs a:hover{color:var(--ink)}
203
+ .crumbs .sep{color:var(--faint)}
204
+ .crumbs [aria-current="page"]{color:var(--ink);font-weight:600}
205
+ .pagination{display:flex;align-items:center;gap:4px;list-style:none;margin:0;padding:0}
206
+ .pagination a,.pagination span{min-width:32px;height:32px;padding:0 8px;border:1px solid var(--line);
207
+ border-radius:var(--r-sm);background:var(--card);font-size:var(--t-sm);color:var(--ink-2);cursor:pointer;display:grid;place-items:center;text-decoration:none}
208
+ .pagination a:hover{border-color:var(--ink)}
209
+ .pagination [aria-current="page"]{background:var(--ink);color:var(--paper);border-color:var(--ink);cursor:default}
210
+ .menu{min-width:180px;background:var(--card);border:1px solid var(--line-2);border-radius:var(--r);
211
+ box-shadow:var(--shadow);padding:6px;list-style:none;margin:0}
212
+ .menu .menu-label{font-size:var(--t-cap);color:var(--faint);text-transform:uppercase;letter-spacing:.06em;padding:6px 10px 2px}
213
+ .menu a,.menu button{display:flex;align-items:center;gap:10px;width:100%;text-align:left;
214
+ padding:7px 10px;border:0;border-radius:var(--r-sm);background:none;color:var(--ink-2);font-size:var(--t-sm);cursor:pointer;text-decoration:none}
215
+ .menu a:hover,.menu button:hover{background:var(--paper-raised);color:var(--ink)}
216
+ .menu .menu-sep{height:1px;background:var(--line);margin:6px 0;border:0}
217
+ .menu .danger{color:var(--alert)}
218
+ .seg{display:inline-flex;background:var(--paper-raised);border:1px solid var(--line-2);border-radius:var(--r);padding:2px;gap:2px}
219
+ .seg button{border:0;background:none;border-radius:6px;padding:5px 12px;font:600 var(--t-sm)/1 var(--sans);color:var(--ink-2);cursor:pointer}
220
+ .seg button[aria-pressed="true"]{background:var(--card);color:var(--ink);box-shadow:var(--shadow-sm)}
221
+ .stepper{display:flex;gap:0;list-style:none;margin:0;padding:0;flex-wrap:wrap}
222
+ .stepper li{display:flex;align-items:center;gap:8px;padding:0 16px 8px 0;color:var(--muted);font-size:var(--t-sm)}
223
+ .stepper .dot{width:24px;height:24px;border-radius:var(--r-pill);border:1px solid var(--line-2);display:grid;
224
+ place-items:center;font-family:var(--mono);font-size:12px;background:var(--card);color:var(--muted)}
225
+ .stepper li.done .dot{background:var(--ready);border-color:var(--ready);color:#fff}
226
+ .stepper li.current .dot{background:var(--ink);border-color:var(--ink);color:var(--paper)}
227
+ .stepper li.current{color:var(--ink);font-weight:600}
228
+ .toolbar{display:flex;align-items:center;gap:2px;flex-wrap:wrap}
229
+ .toolbar .tool{display:grid;place-items:center;width:30px;height:30px;border:1px solid transparent;
230
+ border-radius:var(--r-sm);background:none;cursor:pointer;color:var(--ink-2)}
231
+ .toolbar .tool:hover{background:var(--paper-raised);border-color:var(--line)}
232
+ .toolbar .tool-sep{width:1px;height:18px;background:var(--line);margin:0 4px}
233
+ .outline{font-size:var(--t-sm);border-left:1px solid var(--line);list-style:none;margin:0;padding:0}
234
+ .outline a{display:block;padding:4px 0 4px 14px;color:var(--ink-2);text-decoration:none;
235
+ border-left:2px solid transparent;margin-left:-1px}
236
+ .outline a:hover{color:var(--ink)}
237
+ .outline a[aria-current="true"]{border-left-color:var(--accent);color:var(--ink);font-weight:600}
238
+ .outline a.l2{padding-left:28px;color:var(--muted)}
239
+ .outline a.l3{padding-left:42px;color:var(--muted)}
240
+
241
+ /* Meter */
242
+ .meter{display:block;min-width:0}
243
+ .meter-label{display:flex;justify-content:space-between;font-size:var(--t-cap);color:var(--muted);margin-bottom:5px}
244
+ .meter-track{height:8px;background:var(--paper-raised);border:1px solid var(--line);border-radius:var(--r-pill);overflow:hidden}
245
+ .meter-fill{height:100%;background:var(--ink);border-radius:var(--r-pill)}
246
+ .meter-fill.under{background:var(--progress)}
247
+ .meter-fill.at{background:var(--ready)}
248
+ .meter-fill.over{background:var(--accent)}
249
+
250
+ /* Savestate */
251
+ .savestate{display:inline-flex;align-items:center;gap:6px;font-size:var(--t-cap);color:var(--muted)}
252
+ .savestate::before{content:"";width:7px;height:7px;border-radius:var(--r-pill);background:var(--ready);flex:none}
253
+ .savestate.saving{color:var(--progress)}.savestate.saving::before{background:var(--progress)}
254
+ .savestate.unsaved{color:var(--ink-2)}.savestate.unsaved::before{background:var(--faint)}
255
+
256
+ /* Diff */
257
+ .diff{border:1px solid var(--line);border-radius:var(--r-lg);overflow:hidden;font-family:var(--mono);font-size:12.5px}
258
+ .diff-file{background:var(--paper);border-bottom:1px solid var(--line);padding:8px 12px;font-family:var(--sans);font-size:var(--t-sm);font-weight:600}
259
+ .diff-row{display:grid;grid-template-columns:34px 1fr;border-top:1px solid var(--line)}
260
+ .diff-sign{text-align:center;color:var(--faint);background:var(--paper-raised);user-select:none}
261
+ .diff-text{padding:2px 10px;white-space:pre-wrap;overflow-wrap:anywhere}
262
+ .diff-add{background:var(--ready-bg)}.diff-add .diff-sign{color:var(--ready)}
263
+ .diff-del{background:var(--alert-bg)}.diff-del .diff-sign{color:var(--alert)}
264
+ .diff-ctx .diff-text{color:var(--muted)}
265
+
266
+ /* Build + chapter rows */
267
+ .buildrow{display:grid;grid-template-columns:74px 1fr auto;align-items:center;gap:var(--s-4);
268
+ padding:12px var(--s-4);border:1px solid var(--line);border-radius:var(--r);background:var(--card)}
269
+ .buildrow .fmt{font-family:var(--mono);font-size:var(--t-cap);font-weight:600;letter-spacing:.04em}
270
+ .chapter-row{display:grid;grid-template-columns:auto auto 1fr auto auto;align-items:center;gap:var(--s-3);
271
+ padding:10px 12px;border:1px solid var(--line);border-radius:var(--r);background:var(--card);list-style:none}
272
+ .chapter-row .drag{cursor:grab;color:var(--faint)}
273
+ .chapter-row .idx{font-family:var(--mono);font-size:var(--t-cap);color:var(--faint)}
274
+ .chapter-row .title{font-size:var(--t-sm);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
275
+ .chapter-row .moves{display:flex;gap:2px}
276
+ .chapter-row .moves button{width:24px;height:24px;border:1px solid var(--line);border-radius:var(--r-sm);background:var(--card);cursor:pointer;color:var(--ink-2)}
277
+
278
+ /* Overlays: dialog / drawer / popover */
279
+ .cdialog{padding:0;border:0;background:transparent;max-width:480px;width:calc(100% - 2rem)}
280
+ .cdialog::backdrop{background:rgba(21,23,28,.34)}
281
+ .dialog{width:100%;max-width:480px;background:var(--card);border:1px solid var(--line-2);
282
+ border-radius:var(--r-lg);box-shadow:var(--shadow-pop);overflow:hidden;display:flex;flex-direction:column;max-height:80vh}
283
+ .dialog-head{display:flex;align-items:flex-start;gap:var(--s-4);padding:var(--s-5) var(--s-5) var(--s-3)}
284
+ .dialog-head h2{font-family:var(--display);font-size:19px}
285
+ .dialog-head .x{margin-left:auto;background:none;border:0;cursor:pointer;color:var(--muted);padding:2px;border-radius:var(--r-sm)}
286
+ .dialog-head .x:hover{color:var(--ink)}
287
+ .dialog-body{padding:0 var(--s-5) var(--s-4);overflow:auto;color:var(--ink-2);font-size:var(--t-sm)}
288
+ .drawer{position:fixed;top:0;right:0;bottom:0;width:380px;max-width:90vw;z-index:var(--z-overlay);
289
+ background:var(--card);border-left:1px solid var(--line-2);box-shadow:var(--shadow-pop);display:flex;flex-direction:column}
290
+ .drawer-head{display:flex;align-items:center;gap:var(--s-3);padding:var(--s-4) var(--s-5);border-bottom:1px solid var(--line)}
291
+ .drawer-head h2{font-family:var(--display);font-size:18px}
292
+ .drawer-body{padding:var(--s-5);overflow:auto;flex:1}
293
+ .popover{position:relative;display:inline-block;background:var(--ink);color:var(--paper);
294
+ font-size:var(--t-cap);padding:6px 10px;border-radius:var(--r-sm);box-shadow:var(--shadow);max-width:240px}
295
+
296
+ /* Feedback: toast + banner */
297
+ .toast{display:flex;gap:10px;align-items:flex-start;background:var(--card);border:1px solid var(--line-2);
298
+ border-left-width:3px;border-radius:var(--r);box-shadow:var(--shadow);padding:12px 14px;font-size:var(--t-sm)}
299
+ .toast .body{flex:1;min-width:0}
300
+ .toast .t{font-weight:600}
301
+ .toast-ok{border-left-color:var(--ready)}.toast-ok .ic{color:var(--ready)}
302
+ .toast-alert{border-left-color:var(--alert)}.toast-alert .ic{color:var(--alert)}
303
+ .toast-info{border-left-color:var(--ink)}.toast-info .ic{color:var(--ink)}
304
+ .banner{display:flex;align-items:center;gap:var(--s-3);padding:10px var(--s-5);
305
+ border:1px solid var(--line);border-radius:var(--r);font-size:var(--t-sm)}
306
+ .banner .spacer{flex:1}
307
+ .banner .ic{flex:none}
308
+ .banner-info{background:var(--paper-raised);border-color:var(--line-2);color:var(--ink-2)}
309
+ .banner-warn{background:var(--progress-bg);border-color:var(--progress-line);color:var(--progress)}
310
+ .banner-alert{background:var(--alert-bg);border-color:var(--alert-line);color:var(--alert)}
311
+ .banner-promo{background:var(--ink);border-color:var(--ink);color:var(--paper)}
312
+
313
+ /* App shell chrome (the --shell layout) */
314
+ .app-header{border-bottom:1px solid var(--line);background:color-mix(in srgb,var(--paper) 86%,transparent);
315
+ backdrop-filter:saturate(1.1) blur(8px);position:sticky;top:0;z-index:var(--z-sticky)}
316
+ .app-nav{max-width:var(--shell-max);margin:0 auto;display:flex;align-items:center;
317
+ gap:var(--s-4);padding:var(--s-3) var(--s-5)}
318
+ .app-brand{display:flex;align-items:center;gap:var(--s-2);font-family:var(--display);
319
+ font-weight:600;font-size:18px;letter-spacing:-.01em;text-decoration:none;color:var(--ink)}
320
+ .app-brand .mark{width:13px;height:26px;flex:none;border-radius:2px;background:var(--accent)}
321
+ .app-foot{border-top:1px solid var(--line);margin-top:var(--s-9);padding:var(--s-6) 0;color:var(--muted);font-size:var(--t-sm)}
322
+ }
@@ -0,0 +1,42 @@
1
+ /* ---------------------------------------------------------------------------
2
+ Example theme: "quire" — warm paper / garnet / Spectral + IBM Plex.
3
+
4
+ The worked proof that retheming the whole seams design system is a TOKEN
5
+ OVERRIDE and nothing more. This file overrides ONLY the @theme tokens the
6
+ neutral default declared; it touches no component CSS, no markup, no helper.
7
+ Every ui_* component reskins because each reads the short aliases (var(--ink),
8
+ var(--accent) …), which resolve to these overridden @theme values.
9
+
10
+ To apply it, add ONE line to app/assets/tailwind/application.css, AFTER the
11
+ "seams:design tokens" block (so these win):
12
+
13
+ @import "themes/quire";
14
+
15
+ To go back to the neutral default, delete that line. That is the entire
16
+ retheme surface. See doc/design-system/DESIGN_SYSTEM_THEMING.md.
17
+
18
+ Lineage: this is quire-saas's original Compositor palette + type pairing, the
19
+ design system this engine was extracted from. --------------------------- */
20
+ @theme {
21
+ --color-paper:#fbf8f3; --color-paper-raised:#f4efe7; --color-card:#ffffff;
22
+ --color-ink:#1a1714; --color-ink-2:#423b33; --color-muted:#72675b; --color-faint:#797166;
23
+ --color-line:#e7ded2; --color-line-2:#d8ccba;
24
+ /* The neutral default's accent -> quire's garnet. The component layer reads
25
+ var(--accent) (aliased from --color-accent), so this is the only colour
26
+ swap the accent needs. */
27
+ --color-accent:#7a1f1f; --color-accent-deep:#591616;
28
+ --color-ready:#047857; --color-ready-bg:#e6f4ec; --color-ready-line:#a7d8bd;
29
+ --color-progress:#aa4e08; --color-progress-bg:#fbf1e0; --color-progress-line:#eccf97;
30
+ --color-alert:#cb2323; --color-alert-bg:#fbeceb; --color-alert-line:#f1b6b3;
31
+ /* Display face -> Spectral (a serif); UI + data -> IBM Plex. Self-host the
32
+ faces or load them in the host layout; the token is the single switch. */
33
+ --color-focus:#1a1714;
34
+ --font-display:"Spectral",Georgia,"Times New Roman",serif;
35
+ --font-sans:"IBM Plex Sans",system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
36
+ --font-mono:"IBM Plex Mono",ui-monospace,SFMono-Regular,Menlo,monospace;
37
+ }
38
+
39
+ /* quire pinned the focus ring to ink rather than the accent. The alias layer
40
+ reads --focus, so override that one alias to match. (Everything else inherits
41
+ from the @theme overrides above.) */
42
+ :root{ --focus:var(--color-ink); }
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The starter signed-in dashboard shipped by `bin/seams design --shell`.
4
+ #
5
+ # It is intentionally a thin, host-owned starting point: it subclasses the
6
+ # host's ApplicationController (so any auth / account / tenancy filters the host
7
+ # adds later apply automatically), and renders an empty-state built from the
8
+ # ui_* components that lists the canonical seams engines the host could add.
9
+ #
10
+ # Replace this with your real home page; it exists so a freshly generated host
11
+ # boots to a styled, signed-in screen instead of bare markup. The route + root
12
+ # the generator drew into the host point here.
13
+ module Design
14
+ class DashboardController < ApplicationController
15
+ def index
16
+ # The canonical seams engines a host can generate next. Each entry is the
17
+ # `bin/seams <name>` verb plus a one-line summary, rendered as the
18
+ # dashboard's empty-state list. Static on purpose — a host replaces this
19
+ # controller with its real dashboard once it has data to show.
20
+ @suggested_engines = [
21
+ ["auth", "Sign-in, sessions, password reset, OAuth"],
22
+ ["accounts", "User accounts + the Current attributes"],
23
+ ["teams", "Teams, memberships, invitations"],
24
+ ["billing", "Plans, subscriptions, invoices"],
25
+ ["notifications", "In-app + email notifications"],
26
+ ["admin", "A back-office for the above"]
27
+ ]
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Design
4
+ # The living gallery: renders every component from its preview partial, so the
5
+ # documentation cannot drift from the implementation and a newly added
6
+ # component (a partial + a preview under app/views/ui/) appears here with no
7
+ # extra wiring.
8
+ #
9
+ # Dev/test only — `guide_available?` returns 404 outside Rails.env.local?, so
10
+ # the page never ships to production. It subclasses ActionController::Base
11
+ # (NOT the host's ApplicationController) so it carries no auth, account or
12
+ # tenancy filters: the guide is a pure render of the design system.
13
+ class GuideController < ActionController::Base
14
+ layout "design/guide"
15
+
16
+ before_action :guide_available?
17
+
18
+ def index
19
+ @components = Design.component_names
20
+ end
21
+
22
+ private
23
+
24
+ def guide_available?
25
+ head :not_found unless Rails.env.local?
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Design
4
+ # The design-system form builder. Subclasses the standard Rails builder and
5
+ # only ADDS ui_* methods — f.text_field / f.select / f.submit are untouched —
6
+ # so it is safe to make the app-wide default (see
7
+ # config.action_view.default_form_builder), and you never pass `builder:` per
8
+ # form.
9
+ #
10
+ # Each method wires the form object's name, value and validation error into the
11
+ # ui/field markup, so the accessibility (label association, aria-invalid,
12
+ # aria-describedby) is baked in and cannot drift. Arbitrary HTML options —
13
+ # including data-* attributes for Stimulus — pass straight through to the
14
+ # control, so Hotwire keeps working untouched.
15
+ #
16
+ # Phase 1 ships the field helpers + the shared accessible wrapper; #19 ships
17
+ # the ui/field partial and the rest of the form-component set.
18
+ class FormBuilder < ActionView::Helpers::FormBuilder
19
+ # f.ui_text_field :title, label: "Book title", hint: "Shown on the cover"
20
+ # (+ _email_field / _url_field / _tel_field / _number_field / _password_field)
21
+ %i[text email url tel number password].each do |kind|
22
+ define_method(:"ui_#{kind}_field") do |method, label:, hint: nil, **opts|
23
+ @template.render("ui/field",
24
+ label: label,
25
+ name: field_name(method),
26
+ id: field_id(method),
27
+ type: kind.to_s,
28
+ value: object_value(method),
29
+ hint: error_for(method) ? nil : hint,
30
+ error: error_for(method),
31
+ **opts)
32
+ end
33
+ end
34
+
35
+ # f.ui_text_area :notes, label: "Notes", hint: "Optional"
36
+ def ui_text_area(method, label:, hint: nil, **opts)
37
+ ui_control(method, label:, hint:) do |ids|
38
+ @template.text_area(@object_name, method,
39
+ class: "textarea", id: ids[:id],
40
+ "aria-invalid": ids[:invalid], "aria-describedby": ids[:described],
41
+ **opts)
42
+ end
43
+ end
44
+
45
+ # f.ui_select :marketplace, %w[UK US], label: "Primary marketplace"
46
+ def ui_select(method, choices = nil, options = {}, label:, hint: nil, **html)
47
+ ui_control(method, label:, hint:) do |ids|
48
+ @template.select(@object_name, method, choices, options,
49
+ { class: "select", id: ids[:id],
50
+ "aria-invalid": ids[:invalid], "aria-describedby": ids[:described] }.merge(html))
51
+ end
52
+ end
53
+
54
+ # f.ui_submit "Create" — primary by default; pass variant: for others.
55
+ def ui_submit(value = nil, variant: :primary, **opts)
56
+ classes = ["btn", ("btn-#{variant}" unless variant.to_s == "default"), opts.delete(:class)].compact.join(" ")
57
+ submit(value, class: classes, **opts)
58
+ end
59
+
60
+ private
61
+
62
+ # Shared, accessible .field wrapper for controls the field partial doesn't
63
+ # render itself (textarea, select). Keeps the label/error/aria wiring in one
64
+ # place.
65
+ def ui_control(method, label:, hint:)
66
+ error = error_for(method)
67
+ input_id = field_id(method)
68
+ described = error ? "#{input_id}-error" : (hint ? "#{input_id}-hint" : nil)
69
+ ids = { id: input_id, invalid: (error ? "true" : nil), described: described }
70
+
71
+ @template.content_tag(:div, class: ["field", ("field-error" if error)].compact.join(" ")) do
72
+ parts = [@template.label(@object_name, method, label, class: "label", for: input_id)]
73
+ parts << yield(ids)
74
+ parts << @template.content_tag(:p, hint, class: "hint", id: "#{input_id}-hint") if hint && !error
75
+ parts << @template.content_tag(:p, error, class: "hint", id: "#{input_id}-error") if error
76
+ @template.safe_join(parts)
77
+ end
78
+ end
79
+
80
+ def object_value(method)
81
+ object.respond_to?(method) ? object.public_send(method) : nil
82
+ end
83
+
84
+ def error_for(method)
85
+ return nil unless object.respond_to?(:errors)
86
+
87
+ object.errors[method].first
88
+ end
89
+ end
90
+ end