plutonium 0.62.2 → 0.64.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 (256) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium/SKILL.md +44 -0
  3. data/.claude/skills/plutonium-app/SKILL.md +3 -3
  4. data/.claude/skills/plutonium-async-interactions/SKILL.md +191 -0
  5. data/.claude/skills/plutonium-auth/SKILL.md +36 -0
  6. data/.claude/skills/plutonium-behavior/SKILL.md +121 -24
  7. data/.claude/skills/plutonium-kanban/SKILL.md +17 -3
  8. data/.claude/skills/plutonium-resource/SKILL.md +259 -12
  9. data/.claude/skills/plutonium-tenancy/SKILL.md +32 -3
  10. data/.claude/skills/plutonium-ui/SKILL.md +115 -14
  11. data/.claude/skills/plutonium-wizard/SKILL.md +73 -4
  12. data/.standard.yml +1 -1
  13. data/CHANGELOG.md +73 -0
  14. data/CLAUDE.md +87 -0
  15. data/Rakefile +34 -0
  16. data/SECURITY.md +1 -1
  17. data/app/assets/plutonium.css +1 -1
  18. data/app/assets/plutonium.js +685 -102
  19. data/app/assets/plutonium.js.map +4 -4
  20. data/app/assets/plutonium.min.js +53 -53
  21. data/app/assets/plutonium.min.js.map +4 -4
  22. data/app/views/rodauth/_login_form.html.erb +13 -0
  23. data/db/migrate/async_interactions/20260817000001_create_plutonium_async_runs.rb +170 -0
  24. data/docs/.vitepress/config.ts +81 -3
  25. data/docs/.vitepress/theme/blog.data.ts +44 -0
  26. data/docs/.vitepress/theme/components/BlogIndex.vue +87 -0
  27. data/docs/.vitepress/theme/components/BlogMeta.vue +47 -0
  28. data/docs/.vitepress/theme/components/HomeFeatureTour.vue +293 -0
  29. data/docs/.vitepress/theme/components/HomeHero.vue +3 -3
  30. data/docs/.vitepress/theme/components/HomeInTheBox.vue +8 -0
  31. data/docs/.vitepress/theme/components/HomeStopWriting.vue +1 -0
  32. data/docs/.vitepress/theme/components/HomeWhyPlutonium.vue +84 -0
  33. data/docs/.vitepress/theme/index.ts +8 -4
  34. data/docs/blog/association-inputs-post-signed-ids.md +70 -0
  35. data/docs/blog/fix-the-model-not-the-policy.md +122 -0
  36. data/docs/blog/fractional-ordering-runs-out-of-room.md +67 -0
  37. data/docs/blog/half-finished-forms-are-pii.md +76 -0
  38. data/docs/blog/index.md +13 -0
  39. data/docs/blog/interactions-are-presentation-objects.md +152 -0
  40. data/docs/blog/introducing-plutonium.md +253 -0
  41. data/docs/blog/jobs-are-not-permission-snapshots.md +100 -0
  42. data/docs/blog/plutonium-and-ai-agents.md +48 -0
  43. data/docs/blog/realtime-is-one-line-and-four-dependencies.md +70 -0
  44. data/docs/blog/two-forms-one-dom-id.md +69 -0
  45. data/docs/blog/whats-new-async-kanban-wizards.md +130 -0
  46. data/docs/getting-started/tutorial/04-authorization.md +12 -3
  47. data/docs/getting-started/tutorial/06-nested-resources.md +3 -1
  48. data/docs/getting-started/tutorial/07-author-portal.md +2 -2
  49. data/docs/guides/authentication.md +73 -0
  50. data/docs/guides/authorization.md +2 -0
  51. data/docs/guides/creating-packages.md +5 -3
  52. data/docs/guides/custom-actions.md +74 -12
  53. data/docs/guides/customizing-ui.md +9 -2
  54. data/docs/guides/index.md +1 -0
  55. data/docs/guides/kanban.md +7 -5
  56. data/docs/guides/nested-resources.md +11 -1
  57. data/docs/guides/performance.md +104 -0
  58. data/docs/guides/user-invites.md +1 -1
  59. data/docs/guides/wizards.md +9 -1
  60. data/docs/index.md +3 -3
  61. data/docs/public/images/home/tour-actions.png +0 -0
  62. data/docs/public/images/home/tour-async.png +0 -0
  63. data/docs/public/images/home/tour-kanban.png +0 -0
  64. data/docs/public/images/home/tour-tenancy.png +0 -0
  65. data/docs/public/images/home/tour-wizard.png +0 -0
  66. data/docs/public/images/reference/async-progress-page.png +0 -0
  67. data/docs/public/images/reference/async-running-banner.png +0 -0
  68. data/docs/public/templates/experimental.rb +34 -0
  69. data/docs/public/templates/pluton8.rb +14 -0
  70. data/docs/reference/app/portals.md +15 -3
  71. data/docs/reference/auth/accounts.md +19 -0
  72. data/docs/reference/behavior/async-interactions.md +295 -0
  73. data/docs/reference/behavior/controllers.md +17 -4
  74. data/docs/reference/behavior/index.md +7 -1
  75. data/docs/reference/behavior/interactions.md +152 -22
  76. data/docs/reference/configuration.md +5 -0
  77. data/docs/reference/index.md +1 -0
  78. data/docs/reference/kanban/dsl.md +7 -4
  79. data/docs/reference/kanban/index.md +1 -1
  80. data/docs/reference/kanban/positioning.md +26 -4
  81. data/docs/reference/positioning.md +568 -0
  82. data/docs/reference/resource/actions.md +97 -4
  83. data/docs/reference/resource/definition.md +181 -9
  84. data/docs/reference/tenancy/invites.md +1 -1
  85. data/docs/reference/tenancy/nested-resources.md +60 -2
  86. data/docs/reference/ui/assets.md +4 -0
  87. data/docs/reference/ui/components.md +57 -4
  88. data/docs/reference/ui/displays.md +20 -10
  89. data/docs/reference/ui/index.md +1 -1
  90. data/docs/reference/wizard/dsl.md +33 -0
  91. data/docs/reference/wizard/storage-config.md +1 -0
  92. data/docs/superpowers/plans/2026-07-16-homepage-depth-upgrade.md +624 -0
  93. data/docs/superpowers/plans/2026-07-16-homepage-depth-upgrade.md.tasks.json +32 -0
  94. data/docs/superpowers/plans/2026-07-31-positioned-drag-and-drop.md +1787 -0
  95. data/docs/superpowers/plans/2026-07-31-positioned-drag-and-drop.md.tasks.json +91 -0
  96. data/docs/superpowers/plans/2026-08-17-async-interactions.md +1414 -0
  97. data/docs/superpowers/plans/2026-08-17-async-interactions.md.tasks.json +66 -0
  98. data/docs/superpowers/specs/2026-07-16-homepage-depth-upgrade-design.md +111 -0
  99. data/docs/superpowers/specs/2026-07-17-action-html-attributes-design.md +124 -0
  100. data/docs/superpowers/specs/2026-07-31-positioned-drag-and-drop-design.md +506 -0
  101. data/docs/superpowers/specs/2026-08-17-async-interactions-design.md +185 -0
  102. data/gemfiles/postgres.gemfile.lock +85 -85
  103. data/gemfiles/rails_7.gemfile.lock +323 -141
  104. data/gemfiles/rails_8.0.gemfile.lock +126 -116
  105. data/gemfiles/rails_8.1.gemfile.lock +127 -117
  106. data/lib/generators/pu/async_interactions/install_generator.rb +111 -0
  107. data/lib/generators/pu/async_interactions/templates/app/controllers/async_runs_controller.rb.tt +15 -0
  108. data/lib/generators/pu/core/typespec/typespec_generator.rb +7 -4
  109. data/lib/generators/pu/invites/install_generator.rb +3 -3
  110. data/lib/generators/pu/invites/templates/packages/invites/app/views/layouts/invites/invitation.html.erb.tt +2 -2
  111. data/lib/generators/pu/lib/plutonium_generators/concerns/mounts_engines.rb +47 -2
  112. data/lib/generators/pu/lib/plutonium_generators/concerns/resource_registration.rb +41 -0
  113. data/lib/generators/pu/lite/litestream/litestream_generator.rb +1 -1
  114. data/lib/generators/pu/lite/solid_queue/solid_queue_generator.rb +1 -1
  115. data/lib/generators/pu/res/conn/conn_generator.rb +19 -39
  116. data/lib/generators/pu/res/conn/templates/app/controllers/resource_controller.rb.tt +4 -0
  117. data/lib/generators/pu/rodauth/templates/app/rodauth/account_rodauth_plugin.rb.tt +15 -6
  118. data/lib/generators/pu/rodauth/templates/app/rodauth/rodauth_plugin.rb.tt +7 -0
  119. data/lib/generators/pu/saas/welcome/templates/app/views/layouts/welcome.html.erb.tt +2 -2
  120. data/lib/generators/pu/wizards/install_generator.rb +78 -0
  121. data/lib/plutonium/action/base.rb +71 -9
  122. data/lib/plutonium/action/interactive.rb +9 -0
  123. data/lib/plutonium/attachments.rb +254 -0
  124. data/lib/plutonium/configuration.rb +82 -1
  125. data/lib/plutonium/core/controller.rb +50 -7
  126. data/lib/plutonium/core/controllers/authorizable.rb +16 -0
  127. data/lib/plutonium/core/controllers/entity_scoping.rb +12 -2
  128. data/lib/plutonium/definition/base.rb +51 -0
  129. data/lib/plutonium/definition/display_layout.rb +112 -0
  130. data/lib/plutonium/definition/index_views.rb +8 -7
  131. data/lib/plutonium/definition/input_aliases.rb +38 -0
  132. data/lib/plutonium/definition/page_widths.rb +65 -0
  133. data/lib/plutonium/definition/positioning.rb +126 -0
  134. data/lib/plutonium/definition/sorting.rb +17 -2
  135. data/lib/plutonium/helpers/turbo_helper.rb +7 -0
  136. data/lib/plutonium/interaction/README.md +61 -24
  137. data/lib/plutonium/interaction/async/configuration.rb +38 -0
  138. data/lib/plutonium/interaction/async/context.rb +419 -0
  139. data/lib/plutonium/interaction/async/executor.rb +422 -0
  140. data/lib/plutonium/interaction/async/job.rb +80 -0
  141. data/lib/plutonium/interaction/async/reap_job.rb +81 -0
  142. data/lib/plutonium/interaction/async/run.rb +394 -0
  143. data/lib/plutonium/interaction/async/run_definition.rb +155 -0
  144. data/lib/plutonium/interaction/async/run_policy.rb +86 -0
  145. data/lib/plutonium/interaction/base.rb +34 -7
  146. data/lib/plutonium/interaction/concerns/dispatchable.rb +518 -0
  147. data/lib/plutonium/interaction/concerns/scoping.rb +70 -9
  148. data/lib/plutonium/interaction/response/redirect.rb +11 -3
  149. data/lib/plutonium/kanban/board.rb +14 -0
  150. data/lib/plutonium/kanban/column.rb +4 -2
  151. data/lib/plutonium/kanban/dsl.rb +4 -1
  152. data/lib/plutonium/kanban/grouping.rb +9 -22
  153. data/lib/plutonium/kanban/positioning.rb +5 -65
  154. data/lib/plutonium/positioning/config.rb +94 -0
  155. data/lib/plutonium/positioning/model.rb +128 -0
  156. data/lib/plutonium/positioning.rb +25 -86
  157. data/lib/plutonium/railtie.rb +1 -0
  158. data/lib/plutonium/resource/controller.rb +118 -38
  159. data/lib/plutonium/resource/controllers/crud_actions/index_action.rb +32 -2
  160. data/lib/plutonium/resource/controllers/crud_actions.rb +30 -2
  161. data/lib/plutonium/resource/controllers/eager_loading.rb +87 -0
  162. data/lib/plutonium/resource/controllers/export_csv.rb +10 -1
  163. data/lib/plutonium/resource/controllers/kanban_actions.rb +53 -14
  164. data/lib/plutonium/resource/controllers/position_actions.rb +390 -0
  165. data/lib/plutonium/resource/controllers/presentable.rb +19 -13
  166. data/lib/plutonium/resource/controllers/queryable.rb +5 -1
  167. data/lib/plutonium/resource/controllers/wizard_actions.rb +21 -0
  168. data/lib/plutonium/resource/policy.rb +33 -0
  169. data/lib/plutonium/resource/query_object.rb +36 -0
  170. data/lib/plutonium/routing/mapper_extensions.rb +100 -8
  171. data/lib/plutonium/routing/route_set_extensions.rb +15 -1
  172. data/lib/plutonium/routing/wizard_registration.rb +4 -0
  173. data/lib/plutonium/testing/resource_policy.rb +6 -2
  174. data/lib/plutonium/ui/action_button.rb +12 -7
  175. data/lib/plutonium/ui/actions_dropdown.rb +1 -1
  176. data/lib/plutonium/ui/block.rb +21 -1
  177. data/lib/plutonium/ui/breadcrumbs.rb +187 -55
  178. data/lib/plutonium/ui/component/methods.rb +5 -0
  179. data/lib/plutonium/ui/component/positionable.rb +112 -0
  180. data/lib/plutonium/ui/component/resolves_tags.rb +57 -0
  181. data/lib/plutonium/ui/component/section.rb +185 -0
  182. data/lib/plutonium/ui/display/base.rb +13 -1
  183. data/lib/plutonium/ui/display/components/formatted_value.rb +26 -0
  184. data/lib/plutonium/ui/display/components/section.rb +18 -0
  185. data/lib/plutonium/ui/display/resource.rb +141 -22
  186. data/lib/plutonium/ui/display/theme.rb +20 -1
  187. data/lib/plutonium/ui/export_button.rb +1 -1
  188. data/lib/plutonium/ui/form/base.rb +8 -7
  189. data/lib/plutonium/ui/form/components/intl_tel_input.rb +1 -1
  190. data/lib/plutonium/ui/form/components/section.rb +7 -62
  191. data/lib/plutonium/ui/form/components/uppy.rb +12 -1
  192. data/lib/plutonium/ui/form/concerns/renders_nested_resource_fields.rb +16 -3
  193. data/lib/plutonium/ui/form/concerns/renders_structured_inputs.rb +5 -1
  194. data/lib/plutonium/ui/form/query.rb +2 -4
  195. data/lib/plutonium/ui/form/resource.rb +92 -15
  196. data/lib/plutonium/ui/form/theme.rb +17 -0
  197. data/lib/plutonium/ui/form/wizard.rb +25 -1
  198. data/lib/plutonium/ui/grid/card.rb +79 -11
  199. data/lib/plutonium/ui/grid/resource.rb +47 -5
  200. data/lib/plutonium/ui/interaction/async/run_progress.rb +227 -0
  201. data/lib/plutonium/ui/interaction/async/running_banner.rb +65 -0
  202. data/lib/plutonium/ui/kanban/card.rb +2 -1
  203. data/lib/plutonium/ui/kanban/column.rb +12 -6
  204. data/lib/plutonium/ui/kanban/resource.rb +6 -7
  205. data/lib/plutonium/ui/layout/base.rb +10 -3
  206. data/lib/plutonium/ui/nav_grid_menu.rb +1 -0
  207. data/lib/plutonium/ui/page/base.rb +19 -0
  208. data/lib/plutonium/ui/page/edit.rb +4 -1
  209. data/lib/plutonium/ui/page/index.rb +69 -18
  210. data/lib/plutonium/ui/page/interactive_action.rb +5 -1
  211. data/lib/plutonium/ui/page/new.rb +4 -1
  212. data/lib/plutonium/ui/page/show.rb +27 -10
  213. data/lib/plutonium/ui/page/wizard.rb +10 -1
  214. data/lib/plutonium/ui/page/wizard_chooser.rb +36 -11
  215. data/lib/plutonium/ui/page_width.rb +58 -0
  216. data/lib/plutonium/ui/table/base.rb +34 -1
  217. data/lib/plutonium/ui/table/components/attachment.rb +1 -1
  218. data/lib/plutonium/ui/table/components/bulk_actions_toolbar.rb +32 -8
  219. data/lib/plutonium/ui/table/components/drag_handle.rb +120 -0
  220. data/lib/plutonium/ui/table/components/filter_form.rb +1 -4
  221. data/lib/plutonium/ui/table/components/filter_pills.rb +1 -1
  222. data/lib/plutonium/ui/table/components/row_actions_dropdown.rb +1 -1
  223. data/lib/plutonium/ui/table/resource.rb +50 -5
  224. data/lib/plutonium/ui/table/theme.rb +59 -2
  225. data/lib/plutonium/ui/wizard/review.rb +4 -2
  226. data/lib/plutonium/ui/wizard/summary_display.rb +42 -14
  227. data/lib/plutonium/version.rb +1 -1
  228. data/lib/plutonium/wizard/attachments.rb +32 -197
  229. data/lib/plutonium/wizard/base.rb +6 -1
  230. data/lib/plutonium/wizard/configuration.rb +12 -0
  231. data/lib/plutonium/wizard/controller.rb +14 -0
  232. data/lib/plutonium/wizard/driving.rb +99 -15
  233. data/lib/plutonium/wizard/dsl.rb +23 -0
  234. data/lib/plutonium/wizard/resume.rb +127 -49
  235. data/lib/plutonium/wizard/runner.rb +46 -2
  236. data/lib/plutonium/wizard/step_adapter.rb +1 -1
  237. data/lib/plutonium/wizard/sweep_job.rb +16 -0
  238. data/lib/plutonium.rb +21 -0
  239. data/lib/rodauth/features/session_isolation.rb +92 -0
  240. data/lib/rodauth/plugins.rb +1 -0
  241. data/package.json +2 -1
  242. data/plutonium.gemspec +20 -20
  243. data/src/css/components.css +89 -1
  244. data/src/css/slim_select.css +20 -0
  245. data/src/js/controllers/breadcrumbs_controller.js +112 -0
  246. data/src/js/controllers/bulk_actions_controller.js +10 -2
  247. data/src/js/controllers/kanban_controller.js +30 -21
  248. data/src/js/controllers/positioned_controller.js +452 -0
  249. data/src/js/controllers/register_controllers.js +6 -0
  250. data/src/js/controllers/resource_drop_down_controller.js +5 -0
  251. data/src/js/controllers/run_progress_controller.js +73 -0
  252. data/src/js/drag/sortable.js +186 -0
  253. data/yarn.lock +108 -63
  254. metadata +101 -22
  255. data/docs/.vitepress/theme/components/HomeAudienceSplit.vue +0 -53
  256. data/docs/.vitepress/theme/components/HomePillars.vue +0 -42
@@ -0,0 +1,66 @@
1
+ {
2
+ "planPath": "docs/superpowers/plans/2026-08-17-async-interactions.md",
3
+ "tasks": [
4
+ {
5
+ "id": 0,
6
+ "subject": "Task 1: Config, migration and registration",
7
+ "status": "completed",
8
+ "description": "**Goal:** The plutonium_interaction_runs table exists in a host app when the feature is enabled, following the wizard subsystem's pattern.\n\n**Verify:** bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/interaction/runs/configuration_test.rb\n\n```json:metadata\n{\"files\": [\"lib/plutonium/interaction/runs/configuration.rb\", \"db/migrate/interaction_runs/20260817000001_create_plutonium_interaction_runs.rb\", \"lib/plutonium/railtie.rb\", \"lib/plutonium/configuration.rb\"], \"verifyCommand\": \"bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/interaction/runs/configuration_test.rb\", \"acceptanceCriteria\": [\"enabled defaults to false\", \"migration path surfaced only when enabled\", \"polymorphic *_id columns are string\"], \"requiresUserVerification\": false}\n```"
9
+ },
10
+ {
11
+ "id": 1,
12
+ "subject": "Task 2: The Run model",
13
+ "status": "completed",
14
+ "blockedBy": [
15
+ 0
16
+ ],
17
+ "description": "**Goal:** STI base with state transitions, progress accounting and typed options.\n\n**Verify:** bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/interaction/run_test.rb\n\n```json:metadata\n{\"files\": [\"lib/plutonium/interaction/run.rb\"], \"verifyCommand\": \"bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/interaction/run_test.rb\", \"acceptanceCriteria\": [\"start!/finish!/fail! move state\", \"progress_fraction nil for opaque work\", \"record_target_failure! accumulates\", \"targeted? reflects perform_on\"], \"requiresUserVerification\": false}\n```"
18
+ },
19
+ {
20
+ "id": 2,
21
+ "subject": "Task 3: Authorization context and target resolution",
22
+ "status": "completed",
23
+ "blockedBy": [
24
+ 1
25
+ ],
26
+ "description": "**Goal:** Rebuild (initiator, scoped_entity) in the job and resolve targets through the policy scope. SECURITY CORE \u2014 both failure modes fail open.\n\n**Verify:** bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/interaction/runs/context_test.rb\n\n```json:metadata\n{\"files\": [\"lib/plutonium/interaction/runs/context.rb\", \"test/dummy/app/runs/test_post_run.rb\"], \"verifyCommand\": \"bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/interaction/runs/context_test.rb\", \"acceptanceCriteria\": [\"targets resolve through the policy scope\", \"cross-tenant target not returned\", \"vanished target reported not dropped\", \"single query\"], \"requiresUserVerification\": false}\n```"
27
+ },
28
+ {
29
+ "id": 3,
30
+ "subject": "Task 4: Executor and failure policies",
31
+ "status": "completed",
32
+ "blockedBy": [
33
+ 2
34
+ ],
35
+ "description": "**Goal:** Run the work, honour on_failure (continue/halt/transactional), keep progress current.\n\n**Verify:** bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/interaction/runs/executor_test.rb\n\n```json:metadata\n{\"files\": [\"lib/plutonium/interaction/runs/executor.rb\", \"lib/plutonium/interaction/runs/job.rb\"], \"verifyCommand\": \"bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/interaction/runs/executor_test.rb\", \"acceptanceCriteria\": [\"continue records and proceeds\", \"halt stops at first error\", \"transactional rolls back\", \"missing targets recorded\", \"opaque run performs once\"], \"requiresUserVerification\": false}\n```"
36
+ },
37
+ {
38
+ "id": 4,
39
+ "subject": "Task 5: Interactions dispatch",
40
+ "status": "completed",
41
+ "blockedBy": [
42
+ 3
43
+ ],
44
+ "description": "**Goal:** dispatches_to creates and enqueues a run, then redirects. Outcome stays synchronous.\n\n**Verify:** bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/interaction/dispatchable_test.rb\n\n```json:metadata\n{\"files\": [\"lib/plutonium/interaction/concerns/dispatchable.rb\", \"lib/plutonium/interaction/base.rb\"], \"verifyCommand\": \"bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/interaction/dispatchable_test.rb\", \"acceptanceCriteria\": [\"run created with validated options\", \"initiator and scoped_entity persisted\", \"success outcome redirects to run\", \"invalid interaction enqueues nothing\"], \"requiresUserVerification\": false}\n```"
45
+ },
46
+ {
47
+ "id": 5,
48
+ "subject": "Task 6: The run as a resource, and its progress page",
49
+ "status": "completed",
50
+ "blockedBy": [
51
+ 4
52
+ ],
53
+ "description": "**Goal:** Portal-registerable run resource whose show page is the progress page and refreshes itself while in progress.\n\n**Verify:** bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/interaction/runs/policy_test.rb test/integration/admin_portal/interaction_run_progress_test.rb\n\n```json:metadata\n{\"files\": [\"lib/plutonium/interaction/runs/definition.rb\", \"lib/plutonium/interaction/runs/policy.rb\", \"lib/plutonium/ui/interaction/run_progress.rb\", \"test/dummy/packages/admin_portal/config/routes.rb\"], \"verifyCommand\": \"bundle exec appraisal rails-8.1 ruby -Itest test/plutonium/interaction/runs/policy_test.rb test/integration/admin_portal/interaction_run_progress_test.rb\", \"acceptanceCriteria\": [\"relation_scope limits to entity scope\", \"cross-tenant run not visible\", \"in-progress page polls, finished does not\", \"indeterminate progress renders a spinner\"], \"requiresUserVerification\": false}\n```"
54
+ },
55
+ {
56
+ "id": 6,
57
+ "subject": "Task 7: In-progress runs on the target index",
58
+ "status": "completed",
59
+ "blockedBy": [
60
+ 5
61
+ ],
62
+ "description": "**Goal:** A resource's index lists runs currently working on it, above the collection.\n\n**Verify:** bundle exec appraisal rails-8.1 ruby -Itest test/integration/admin_portal/interaction_run_banner_test.rb\n\n```json:metadata\n{\"files\": [\"lib/plutonium/ui/interaction/running_banner.rb\", \"lib/plutonium/ui/page/index.rb\"], \"verifyCommand\": \"bundle exec appraisal rails-8.1 ruby -Itest test/integration/admin_portal/interaction_run_banner_test.rb\", \"acceptanceCriteria\": [\"lists in-progress runs for this resource\", \"excludes completed and other-resource runs\", \"excludes other tenants\", \"no banner when none running\"], \"requiresUserVerification\": false}\n```"
63
+ }
64
+ ],
65
+ "lastUpdated": "2026-08-17T13:42:04.578647"
66
+ }
@@ -0,0 +1,111 @@
1
+ # Homepage Depth & Proof Upgrade — Design
2
+
3
+ **Date:** 2026-07-16
4
+ **Status:** Approved
5
+ **Scope:** VitePress docs homepage (`docs/index.md` + `docs/.vitepress/theme/components/Home*.vue`)
6
+
7
+ ## Problem
8
+
9
+ The homepage looks good but undersells the product. Two confirmed weaknesses
10
+ (vs Avo/ActiveAdmin/Administrate, Filament/Refine/AdminJS, and Jumpstart/Bullet Train):
11
+
12
+ 1. **Feature depth is hidden.** Wizards, kanban boards, custom/bulk actions, and
13
+ multi-tenancy/nesting barely appear. The surface area reads smaller than it is.
14
+ 2. **Not enough proof.** Claims outnumber evidence — three CRUD screenshots and one
15
+ asciinema cast carry the whole page.
16
+
17
+ Constraint: the overall look (design system, section rhythm, dark-terminal aesthetic)
18
+ stays. No hosted demo. No new screenshot production — reuse existing guide assets.
19
+
20
+ ## Approved changes
21
+
22
+ New page order:
23
+
24
+ ```
25
+ Hero → Stop writing → Walkthrough → Feature tour (NEW) → Why Plutonium (NEW, merged) → In the box → CTA
26
+ ```
27
+
28
+ ### 1. `HomeFeatureTour.vue` (new section)
29
+
30
+ Slots between `HomeWalkthrough` and the merged band.
31
+
32
+ - **Heading:** "More than CRUD." Sub: "Four features other frameworks make you
33
+ build yourself. All declarative. All policy-aware."
34
+ - **Desktop (>768px):** left vertical rail listing the four features, each with a
35
+ one-line hook; right panel shows the selected feature. Selected rail item gets the
36
+ accent left-border treatment (matches `--pu-accent`).
37
+ - **Mobile (≤768px):** the rail reflows into an accordion — all four heads always
38
+ visible, one open at a time, same panel content inside.
39
+ - **Panel anatomy (each feature):**
40
+ 1. File-path caption bar (e.g. `app/definitions/task_definition.rb`)
41
+ 2. DSL code snippet, dark terminal styling (`--pu-bg-dark`), syntax-highlighted
42
+ 3. Real screenshot below the code, reused from `docs/public/images/guides/`
43
+ 4. A one-line "policy-aware" callout: the button/column/step disappears when the
44
+ policy says no
45
+ 5. "Read the guide →" link to the relevant guide page
46
+
47
+ | Rail item | Hook | Snippet | Screenshot | Guide link |
48
+ |---|---|---|---|---|
49
+ | Kanban boards | drag-drop from one block | `kanban do … end` column DSL (todo/doing/done, `wip:`, `role:`) | `guides/kanban-board.png` | `/guides/kanban` |
50
+ | Wizards | multi-step flows, branching | trimmed `CompanyOnboardingWizard` (2 steps + `review` + `execute`) | `guides/wizards-step.png` | `/guides/wizards` |
51
+ | Actions & interactions | business logic + auto UI | `action :publish, interaction: PublishPostInteraction` + `def publish? = update? && record.draft?` | `guides/custom-actions-bulk.png` | `/guides/custom-actions` |
52
+ | Multi-tenancy & nesting | scoping, invites, nesting | entity-scoping snippet (`scoped_to_entity` / portal strategy) | `guides/multi-tenancy-dashboard.png` | `/guides/multi-tenancy` |
53
+
54
+ - Interactivity via a small Vue `ref` for the selected feature (desktop rail and
55
+ mobile accordion share state). No new dependencies.
56
+ - Note: the wizards guide labels the feature experimental; the tour panel copy should
57
+ not contradict that (avoid "stable", fine to omit the caveat).
58
+
59
+ ### 2. `HomeWhyPlutonium.vue` (new, replaces `HomePillars` + `HomeAudienceSplit`)
60
+
61
+ One `pu-section--band` section that merges both messages, cutting a full section of
62
+ page height:
63
+
64
+ - **Top:** the four pillar cards in compact form — Convention over configuration /
65
+ It's just Rails / Multi-tenant ready / AI-readable — same icons and card styling
66
+ as today, tighter padding.
67
+ - **Bottom:** a two-column footer row inside the same section:
68
+ - "For Rails developers" — existing lede + 2 bullets: "Convention extended to
69
+ CRUD, policies, and portals" and "Generated code lives in your repo — edit anything"
70
+ - "For founders & teams" — existing lede + 2 bullets: "Admin panel, signup, and
71
+ invites on day one" and "Multi-tenant scoping when you need it"
72
+ - `HomePillars.vue` and `HomeAudienceSplit.vue` are deleted; `docs/index.md` updated.
73
+
74
+ ### 3. Copy-only weaves (existing components)
75
+
76
+ - **`HomeHero.vue`:** pillars line gains **Wizards.** and **Kanban.** in the bold list.
77
+ - **`HomeStopWriting.vue`:** Plutonium win column gains `+ Actions` (accurate — the
78
+ scaffold ships action wiring).
79
+ - **`HomeInTheBox.vue`:** new fourth category row **Workflows** with three links:
80
+ Wizards (`/guides/wizards`), Kanban (`/guides/kanban`), Interactions
81
+ (`/guides/custom-actions` — there is no separate interactions guide).
82
+
83
+ ### 4. Out of scope
84
+
85
+ - Hosted live demo
86
+ - New screenshot/asset production
87
+ - Full-spine restructure (each feature as a full-width alternating panel) —
88
+ considered and superseded by the rail-tour + tightened-middle structure
89
+ - Credibility signals (testimonials, star counts) and quantified-value claims —
90
+ explicitly not the weak spots being addressed
91
+
92
+ ## Error handling / edge cases
93
+
94
+ - Screenshots must be referenced with `withBase(...)` like existing components.
95
+ - Rail/accordion is progressive: SSR renders the first feature expanded so the
96
+ section is meaningful without JS.
97
+ - Accordion heads are `<button>`s with `aria-expanded`; rail follows the existing
98
+ `role="tablist"` pattern used by `HomeCta.vue` pills.
99
+
100
+ ## Testing / verification
101
+
102
+ - `yarn docs:dev` visual pass at desktop and ≤768px widths (rail ↔ accordion reflow).
103
+ - `yarn docs:build` must pass (dead-link check catches wrong guide paths).
104
+ - Verify all four screenshots resolve under the site base path in the built output.
105
+
106
+ ## Decisions log
107
+
108
+ - Tour layout: feature rail (chosen over tabbed showcase) — breadth visible at a glance
109
+ - Mobile: accordion (chosen over horizontal pill strip) — same breadth rationale
110
+ - Phase 2 pulled forward, resolved as structure A: rail tour + merged middle band
111
+ (chosen over full-spine panels)
@@ -0,0 +1,124 @@
1
+ # Native `link:` / `button:` HTML attributes on actions
2
+
3
+ **Date:** 2026-07-17
4
+ **Status:** Approved (design)
5
+
6
+ ## Problem
7
+
8
+ On released Plutonium (≤0.62.2) there is no native way to put `target`, `rel`, or
9
+ arbitrary HTML attributes on an action's rendered control. `Action::Base` reads a
10
+ fixed key set in `initialize`, so any extra option passed to an action is silently
11
+ inert — not stored, never rendered. `ActionButton`'s three render paths
12
+ (`render_link`, `render_button`, `render_dropdown_item`) hardcode their attributes.
13
+
14
+ Concrete need: an action that opens an external link in a new tab
15
+ (`target: "_blank" rel: "noopener noreferrer"`), or that carries custom
16
+ `data-*` / `aria-*` attributes.
17
+
18
+ ## API
19
+
20
+ Two target-named attribute bags on the action DSL, each naming the **element**
21
+ it applies to. No merged/"always" bag — `link:` covers every anchor rendering
22
+ and `button:` covers the button_to form, so per-element bags cover everything
23
+ with no "which element?" ambiguity.
24
+
25
+ The element rule, not HTTP method, decides which bag applies: dropdown items
26
+ are anchors even for non-GET actions (they submit via `data-turbo-method`), so
27
+ `link:` applies to them regardless of method, and `button:` applies only to the
28
+ toolbar's button_to rendering. A non-GET action that needs attributes in both
29
+ placements sets both bags.
30
+
31
+ ```ruby
32
+ action :docs,
33
+ link: { target: "_blank", rel: "noopener noreferrer", data: { analytics: "docs" } },
34
+ button: { data: { turbo_confirm: "Sure?" } }
35
+ ```
36
+
37
+ - **`link:`** → every `<a>` rendered for the action: the GET toolbar link,
38
+ dropdown items (any method), bulk-actions toolbar links, kanban column
39
+ action links, and the grid/kanban card's hidden show anchor (for `:show`).
40
+ - **`button:`** → the `button_to` **`<form>`** element, in `render_button` (non-GET),
41
+ via `button_to`'s `form:` option. The form wrapper, not the inner `<button>`.
42
+ - Both default to `{}`. `target`/`rel` are just keys inside `link:` — no first-class
43
+ sugar, nothing special-cased.
44
+
45
+ ## Storage — `Action::Base`
46
+
47
+ - New ivars `@link` / `@button` in `initialize`, each normalized on the way in:
48
+ the action stores its **own deep-symbolized copy** of the bag (never freezing
49
+ or sharing the caller's hash), structurally deep-frozen. Symbolizing is
50
+ load-bearing: the render-time `deep_merge` matches keys exactly, so a
51
+ string-keyed bag would sit *alongside* the framework's symbol keys and emit
52
+ duplicate attributes (browser keeps the first — the framework's) instead of
53
+ overriding. Any non-Hash value — including `false` — raises `ArgumentError`.
54
+ - Matching `attr_reader :link, :button`, plus the merge points
55
+ `link_attributes(base)` / `button_attributes(base)`: rendering surfaces hand
56
+ the framework-built attribute hash to the action, which deep-merges its bag
57
+ over it (author wins). Surfaces never merge the raw bags themselves.
58
+ - Both added to `to_options` so `with(**overrides)` round-trips them (miss this and
59
+ `.with` silently drops them — the one non-negotiable).
60
+ - `Action::Interactive` already forwards `**options` to `super`, so the bags flow
61
+ through interactive actions automatically. No change needed there.
62
+ - Verify during implementation that the definition-level `action` /
63
+ `interactive_action` DSL forwards unknown kwargs into the action constructor
64
+ (it splats `**options` into `Action::Simple.new` / `Interactive::Factory.create`,
65
+ so it should — confirm, don't assume).
66
+
67
+ ## Rendering
68
+
69
+ Merge rule everywhere: build the framework's attribute hash as the base, then
70
+ hand it to the action's merge point (`action.link_attributes(base)` /
71
+ `action.button_attributes(base)`) — **the author wins on every key**,
72
+ recursively through nested `data`. This gives authors full control (they can
73
+ override `turbo_frame`, `class`, `data-*`, anything) at their own risk.
74
+
75
+ Surfaces funnelling through the merge points:
76
+
77
+ - **`ActionButton#render_link`** — base
78
+ `{ class: button_classes, data: { turbo_frame: … }.merge(@extra_data) }`
79
+ through `link_attributes`, passed to `link_to`.
80
+ - **`ActionButton#render_dropdown_item`** — the existing `link_attrs` hash
81
+ through `link_attributes` before `a(**…)`. Applies to any method — dropdown
82
+ items are anchors; `button:` never applies here.
83
+ - **`ActionButton#render_button`** — the `form:` option hash passed to
84
+ `button_to` through `button_attributes`. So `button: { target: "_top" }` sets
85
+ the form's target; `button: { data: { … } }` deep-merges into the form's data.
86
+ - **`BulkActionsToolbar#render_action_button`** — the bulk-action anchor's
87
+ attrs through `link_attributes`.
88
+ - **`Kanban::Column#column_action_link_attributes`** — the column-action
89
+ anchor's attrs through `link_attributes` (of the registered action).
90
+ - **`Grid::Card#render_show_link`** — the hidden row-click show anchor's attrs
91
+ through the `:show` action's `link_attributes`.
92
+
93
+ ## Tests
94
+
95
+ - `test/plutonium/action/base_test.rb`:
96
+ - `link` / `button` readers default to `{}` and return what was passed.
97
+ - `to_options` includes both bags; `with(link: …)` round-trips and overrides.
98
+ - `ActionButton` rendering tests:
99
+ - `link:` attributes (`target`, `rel`, custom `data-*`) land on the `<a>` in the
100
+ GET-link and dropdown renderings.
101
+ - `button:` attributes land on the `<form>` in the non-GET rendering.
102
+ - Author wins on collision: an author `data.turbo_frame` overrides the
103
+ framework's.
104
+
105
+ ## Scope guard (out)
106
+
107
+ - No third "always" / `html:` bag.
108
+ - No attribute-name validation — authors own their HTML; a bad attribute is their
109
+ problem. (Container shape IS validated: a non-Hash bag raises; keys are
110
+ symbolized so string-keyed bags can't silently lose the merge.)
111
+ - No per-element targeting *within* a rendering (e.g. the inner `<button>` vs the
112
+ `<form>`) — `button:` targets the form only. Can be extended later if needed.
113
+
114
+ ## Known footguns (documented, accepted)
115
+
116
+ - `deep_merge` only recurses when **both** values are hashes: an author scalar
117
+ `data:` (`button: { data: "x" }`) wholesale-replaces the framework's data hash,
118
+ deleting `turbo_confirm`/`turbo_frame`. Pass `data:` as a hash. Same mechanics
119
+ mean `class:` **replaces** the framework's classes (no token append) — that's
120
+ the author-wins contract working as designed.
121
+ - `button:` lands on the `<form>` wrapper, not the visible `<button>`: click-
122
+ element attributes (`aria-label`, `class` for the control) don't reach the
123
+ button element. Documented in the guide; per-element targeting is the
124
+ deferred extension above.