plutonium 0.64.0 → 0.66.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 (344) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium/SKILL.md +44 -41
  3. data/.claude/skills/plutonium-app/SKILL.md +115 -59
  4. data/.claude/skills/plutonium-async-interactions/SKILL.md +19 -19
  5. data/.claude/skills/plutonium-auth/SKILL.md +119 -53
  6. data/.claude/skills/plutonium-behavior/SKILL.md +110 -78
  7. data/.claude/skills/plutonium-dashboard/SKILL.md +168 -0
  8. data/.claude/skills/plutonium-kanban/SKILL.md +81 -49
  9. data/.claude/skills/plutonium-resource/SKILL.md +236 -145
  10. data/.claude/skills/plutonium-tenancy/SKILL.md +104 -55
  11. data/.claude/skills/plutonium-testing/SKILL.md +130 -33
  12. data/.claude/skills/plutonium-ui/SKILL.md +171 -95
  13. data/.claude/skills/plutonium-wizard/SKILL.md +93 -82
  14. data/CHANGELOG.md +80 -0
  15. data/CLAUDE.md +282 -210
  16. data/README.md +9 -9
  17. data/SECURITY.md +1 -1
  18. data/app/assets/plutonium-charts.js +20604 -0
  19. data/app/assets/plutonium-charts.js.map +7 -0
  20. data/app/assets/plutonium-charts.min.js +41 -0
  21. data/app/assets/plutonium-charts.min.js.map +7 -0
  22. data/app/assets/plutonium.css +1 -1
  23. data/app/assets/plutonium.js +794 -629
  24. data/app/assets/plutonium.js.map +4 -4
  25. data/app/assets/plutonium.min.js +48 -48
  26. data/app/assets/plutonium.min.js.map +4 -4
  27. data/app/views/plutonium/_flash_alerts.html.erb +2 -2
  28. data/app/views/plutonium/_resource_header.html.erb +2 -2
  29. data/app/views/plutonium/_resource_sidebar.html.erb +11 -2
  30. data/app/views/plutonium/_toast.html.erb +2 -2
  31. data/app/views/rodauth/_login_form.html.erb +1 -1
  32. data/app/views/rodauth/_password_visibility.html.erb +1 -1
  33. data/app/views/rodauth/add_recovery_codes.html.erb +2 -2
  34. data/app/views/rodauth/change_login.html.erb +2 -2
  35. data/app/views/rodauth/create_account.html.erb +4 -4
  36. data/app/views/rodauth/reset_password_request.html.erb +1 -1
  37. data/app/views/rodauth/verify_account_resend.html.erb +1 -1
  38. data/app/views/rodauth/webauthn_remove.html.erb +1 -1
  39. data/config/brakeman.ignore +4 -4
  40. data/config/initializers/rabl.rb +0 -40
  41. data/config/locales/en/api_client.yml +22 -0
  42. data/config/locales/en/async.yml +34 -0
  43. data/config/locales/en/dashboard.yml +25 -0
  44. data/config/locales/en/invites.yml +37 -0
  45. data/config/locales/en/js.yml +40 -0
  46. data/config/locales/en/kanban.yml +28 -0
  47. data/config/locales/en/plutonium.yml +26 -0
  48. data/config/locales/en/positioning.yml +14 -0
  49. data/config/locales/en/profile.yml +29 -0
  50. data/config/locales/en/resource.yml +60 -0
  51. data/config/locales/en/ui.yml +93 -0
  52. data/config/locales/en/wizard.yml +46 -0
  53. data/docs/.vitepress/config.ts +34 -1
  54. data/docs/.vitepress/sync-skills.mjs +6 -3
  55. data/docs/.vitepress/theme/components/HomeHero.vue +1 -1
  56. data/docs/.vitepress/theme/components/HomeInTheBox.vue +14 -0
  57. data/docs/.vitepress/theme/custom.css +34 -377
  58. data/docs/blog/introducing-plutonium-dashboards.md +174 -0
  59. data/docs/blog/introducing-plutonium-i18n.md +90 -0
  60. data/docs/blog/introducing-plutonium.md +4 -5
  61. data/docs/blog/whats-new-async-kanban-wizards.md +1 -1
  62. data/docs/getting-started/installation.md +5 -5
  63. data/docs/getting-started/tutorial/02-first-resource.md +3 -3
  64. data/docs/getting-started/tutorial/03-authentication.md +7 -7
  65. data/docs/getting-started/tutorial/04-authorization.md +21 -4
  66. data/docs/getting-started/tutorial/05-custom-actions.md +2 -2
  67. data/docs/getting-started/tutorial/06-nested-resources.md +5 -2
  68. data/docs/getting-started/tutorial/07-author-portal.md +2 -2
  69. data/docs/getting-started/tutorial/08-customizing-ui.md +45 -30
  70. data/docs/getting-started/tutorial/index.md +1 -1
  71. data/docs/guides/adding-resources.md +10 -7
  72. data/docs/guides/authentication.md +25 -25
  73. data/docs/guides/authorization.md +24 -24
  74. data/docs/guides/creating-packages.md +17 -17
  75. data/docs/guides/custom-actions.md +32 -32
  76. data/docs/guides/customizing-ui.md +29 -26
  77. data/docs/guides/dashboards.md +300 -0
  78. data/docs/guides/index.md +4 -3
  79. data/docs/guides/kanban.md +56 -56
  80. data/docs/guides/multi-tenancy.md +35 -22
  81. data/docs/guides/nested-resources.md +21 -21
  82. data/docs/guides/performance.md +3 -3
  83. data/docs/guides/search-filtering.md +13 -13
  84. data/docs/guides/testing.md +16 -12
  85. data/docs/guides/theming.md +32 -17
  86. data/docs/guides/troubleshooting.md +2 -2
  87. data/docs/guides/user-invites.md +17 -17
  88. data/docs/guides/user-profile.md +51 -24
  89. data/docs/guides/wizards.md +55 -55
  90. data/docs/public/images/blog/dashboards-card-error.png +0 -0
  91. data/docs/public/images/blog/dashboards-loading.png +0 -0
  92. data/docs/public/images/blog/dashboards-overview.png +0 -0
  93. data/docs/public/images/blog/dashboards-sidebar.png +0 -0
  94. data/docs/public/images/blog/i18n-es-admin.png +0 -0
  95. data/docs/public/images/guides/dashboard-card-error.png +0 -0
  96. data/docs/public/images/guides/dashboard-loading.png +0 -0
  97. data/docs/public/images/guides/dashboard-overview.png +0 -0
  98. data/docs/public/images/guides/dashboard-sidebar.png +0 -0
  99. data/docs/reference/app/generators.md +24 -24
  100. data/docs/reference/app/index.md +15 -18
  101. data/docs/reference/app/packages.md +8 -8
  102. data/docs/reference/app/portals.md +77 -29
  103. data/docs/reference/auth/accounts.md +15 -15
  104. data/docs/reference/auth/index.md +12 -12
  105. data/docs/reference/auth/profile.md +67 -29
  106. data/docs/reference/behavior/async-interactions.md +24 -24
  107. data/docs/reference/behavior/controllers.md +28 -28
  108. data/docs/reference/behavior/index.md +5 -5
  109. data/docs/reference/behavior/interactions.md +44 -44
  110. data/docs/reference/behavior/policies.md +48 -28
  111. data/docs/reference/configuration.md +88 -31
  112. data/docs/reference/dashboard/dsl.md +123 -0
  113. data/docs/reference/dashboard/index.md +35 -0
  114. data/docs/reference/dashboard/registration.md +76 -0
  115. data/docs/reference/generators/lite.md +7 -7
  116. data/docs/reference/i18n.md +262 -0
  117. data/docs/reference/index.md +11 -2
  118. data/docs/reference/kanban/authorization.md +9 -9
  119. data/docs/reference/kanban/dsl.md +33 -33
  120. data/docs/reference/kanban/index.md +1 -1
  121. data/docs/reference/kanban/positioning.md +18 -16
  122. data/docs/reference/resource/actions.md +51 -51
  123. data/docs/reference/resource/definition.md +166 -88
  124. data/docs/reference/resource/export.md +11 -7
  125. data/docs/reference/resource/index.md +16 -14
  126. data/docs/reference/resource/model.md +24 -24
  127. data/docs/reference/{positioning.md → resource/positioning.md} +78 -75
  128. data/docs/reference/resource/query.md +14 -12
  129. data/docs/reference/tenancy/entity-scoping.md +65 -35
  130. data/docs/reference/tenancy/index.md +11 -11
  131. data/docs/reference/tenancy/invites.md +20 -20
  132. data/docs/reference/tenancy/nested-resources.md +13 -13
  133. data/docs/reference/testing/index.md +116 -22
  134. data/docs/reference/ui/assets.md +65 -25
  135. data/docs/reference/ui/components.md +20 -20
  136. data/docs/reference/ui/displays.md +19 -22
  137. data/docs/reference/ui/forms.md +35 -35
  138. data/docs/reference/ui/index.md +17 -15
  139. data/docs/reference/ui/layouts.md +21 -21
  140. data/docs/reference/ui/pages.md +22 -22
  141. data/docs/reference/ui/tables.md +7 -7
  142. data/docs/reference/wizard/anchoring-resume.md +33 -32
  143. data/docs/reference/wizard/dsl.md +45 -45
  144. data/docs/reference/wizard/index.md +6 -6
  145. data/docs/reference/wizard/one-time.md +18 -18
  146. data/docs/reference/wizard/registration-launch.md +32 -32
  147. data/docs/reference/wizard/storage-config.md +23 -23
  148. data/esbuild.config.js +5 -2
  149. data/gemfiles/rails_7.gemfile.lock +2 -2
  150. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  151. data/lib/active_model/validations/array_validator.rb +1 -1
  152. data/lib/active_model/validations/attached_validator.rb +1 -1
  153. data/lib/active_model/validations/url_validator.rb +2 -2
  154. data/lib/generators/pu/core/assets/assets_generator.rb +36 -21
  155. data/lib/generators/pu/core/assets/templates/postcss.config.js +1 -1
  156. data/lib/generators/pu/core/update/update_generator.rb +3 -2
  157. data/lib/generators/pu/dashboard/dashboard_generator.rb +96 -0
  158. data/lib/generators/pu/dashboard/templates/dashboard.rb.tt +25 -0
  159. data/lib/generators/pu/docker/install/install_generator.rb +58 -0
  160. data/lib/generators/pu/docker/install/templates/Dockerfile.dev.tt +18 -3
  161. data/lib/generators/pu/docker/install/templates/Dockerfile.tt +20 -5
  162. data/lib/generators/pu/docker/install/templates/{docker-compose.yml → docker-compose.yml.tt} +1 -1
  163. data/lib/generators/pu/gem/dotenv/dotenv_generator.rb +1 -1
  164. data/lib/generators/pu/invites/invitable_generator.rb +18 -1
  165. data/lib/generators/pu/lib/plutonium_generators/concerns/actions.rb +0 -95
  166. data/lib/generators/pu/lib/plutonium_generators/concerns/js_package_manager.rb +146 -0
  167. data/lib/generators/pu/lib/plutonium_generators/generator.rb +1 -17
  168. data/lib/generators/pu/pkg/package/package_generator.rb +1 -0
  169. data/lib/generators/pu/pkg/package/templates/config/locales/en.yml.tt +28 -0
  170. data/lib/generators/pu/pkg/portal/portal_generator.rb +1 -0
  171. data/lib/generators/pu/pkg/portal/templates/config/locales/en.yml.tt +30 -0
  172. data/lib/generators/pu/profile/conn_generator.rb +6 -0
  173. data/lib/generators/pu/res/scaffold/scaffold_generator.rb +0 -8
  174. data/lib/generators/pu/rodauth/concerns/feature_selector.rb +0 -18
  175. data/lib/plutonium/action/base.rb +27 -3
  176. data/lib/plutonium/action/interactive.rb +17 -4
  177. data/lib/plutonium/api_client/concerns/create_api_client.rb +12 -11
  178. data/lib/plutonium/api_client/concerns/disable_api_client.rb +3 -3
  179. data/lib/plutonium/auth/sequel_adapter.rb +1 -1
  180. data/lib/plutonium/configuration.rb +5 -0
  181. data/lib/plutonium/core/controller.rb +25 -1
  182. data/lib/plutonium/core/controllers/authorizable.rb +1 -1
  183. data/lib/plutonium/dashboard/base.rb +114 -0
  184. data/lib/plutonium/dashboard/card.rb +200 -0
  185. data/lib/plutonium/dashboard/controller.rb +101 -0
  186. data/lib/plutonium/dashboard/dsl.rb +104 -0
  187. data/lib/plutonium/dashboard/register.rb +40 -0
  188. data/lib/plutonium/dashboard/route_resolution.rb +30 -0
  189. data/lib/plutonium/dashboard.rb +42 -0
  190. data/lib/plutonium/definition/actions.rb +6 -1
  191. data/lib/plutonium/definition/base.rb +41 -3
  192. data/lib/plutonium/definition/index_views.rb +1 -1
  193. data/lib/plutonium/definition/positioning.rb +1 -1
  194. data/lib/plutonium/definition/presentable.rb +2 -2
  195. data/lib/plutonium/helpers/assets_helper.rb +15 -2
  196. data/lib/plutonium/helpers/display_helper.rb +10 -2
  197. data/lib/plutonium/interaction/async/context.rb +6 -8
  198. data/lib/plutonium/interaction/async/executor.rb +6 -7
  199. data/lib/plutonium/interaction/async/run.rb +2 -2
  200. data/lib/plutonium/interaction/async/run_definition.rb +1 -1
  201. data/lib/plutonium/interaction/base.rb +2 -0
  202. data/lib/plutonium/interaction/concerns/dispatchable.rb +15 -3
  203. data/lib/plutonium/invites/concerns/cancel_invite.rb +3 -3
  204. data/lib/plutonium/invites/concerns/invite_token.rb +25 -7
  205. data/lib/plutonium/invites/concerns/invite_user.rb +10 -5
  206. data/lib/plutonium/invites/concerns/resend_invite.rb +4 -4
  207. data/lib/plutonium/invites/controller.rb +11 -11
  208. data/lib/plutonium/kanban/column.rb +13 -3
  209. data/lib/plutonium/kanban/dsl.rb +7 -4
  210. data/lib/plutonium/models/has_cents.rb +1 -1
  211. data/lib/plutonium/profile/security_section.rb +10 -18
  212. data/lib/plutonium/query/filter.rb +10 -1
  213. data/lib/plutonium/query/filters/association.rb +27 -2
  214. data/lib/plutonium/query/filters/boolean.rb +4 -4
  215. data/lib/plutonium/query/filters/date.rb +4 -17
  216. data/lib/plutonium/query/filters/date_range.rb +2 -2
  217. data/lib/plutonium/query/filters/select.rb +10 -1
  218. data/lib/plutonium/query/filters/text.rb +9 -12
  219. data/lib/plutonium/railtie.rb +17 -0
  220. data/lib/plutonium/resource/controller.rb +10 -5
  221. data/lib/plutonium/resource/controllers/crud_actions.rb +17 -20
  222. data/lib/plutonium/resource/controllers/export_csv.rb +19 -9
  223. data/lib/plutonium/resource/controllers/kanban_actions.rb +16 -16
  224. data/lib/plutonium/resource/controllers/position_actions.rb +5 -5
  225. data/lib/plutonium/resource/controllers/typeahead.rb +24 -1
  226. data/lib/plutonium/resource/query_object.rb +30 -3
  227. data/lib/plutonium/resource/record/associated_with.rb +23 -2
  228. data/lib/plutonium/routing/dashboard_registration.rb +137 -0
  229. data/lib/plutonium/routing/resource_registration.rb +5 -0
  230. data/lib/plutonium/routing/route_set_extensions.rb +2 -1
  231. data/lib/plutonium/translation.rb +227 -0
  232. data/lib/plutonium/ui/actions_dropdown.rb +2 -2
  233. data/lib/plutonium/ui/breadcrumbs.rb +3 -3
  234. data/lib/plutonium/ui/color_mode_selector.rb +1 -1
  235. data/lib/plutonium/ui/component/behaviour.rb +46 -0
  236. data/lib/plutonium/ui/component/methods.rb +4 -0
  237. data/lib/plutonium/ui/dashboard/board.rb +80 -0
  238. data/lib/plutonium/ui/dashboard/card.rb +127 -0
  239. data/lib/plutonium/ui/dashboard/chart.rb +43 -0
  240. data/lib/plutonium/ui/dashboard/custom.rb +32 -0
  241. data/lib/plutonium/ui/dashboard/frame.rb +25 -0
  242. data/lib/plutonium/ui/dashboard/metric.rb +137 -0
  243. data/lib/plutonium/ui/dashboard/skeleton.rb +40 -0
  244. data/lib/plutonium/ui/display/components/attachment.rb +1 -1
  245. data/lib/plutonium/ui/display/components/badge.rb +7 -1
  246. data/lib/plutonium/ui/display/components/boolean.rb +2 -2
  247. data/lib/plutonium/ui/display/resource.rb +23 -16
  248. data/lib/plutonium/ui/export_button.rb +3 -3
  249. data/lib/plutonium/ui/form/components/key_value_store.rb +5 -5
  250. data/lib/plutonium/ui/form/components/password.rb +2 -2
  251. data/lib/plutonium/ui/form/components/resource_select.rb +27 -0
  252. data/lib/plutonium/ui/form/components/secure_association.rb +2 -0
  253. data/lib/plutonium/ui/form/components/uppy.rb +11 -3
  254. data/lib/plutonium/ui/form/concerns/renders_nested_resource_fields.rb +1 -1
  255. data/lib/plutonium/ui/form/concerns/renders_repeater_row_controls.rb +5 -4
  256. data/lib/plutonium/ui/form/concerns/renders_structured_inputs.rb +1 -1
  257. data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +7 -1
  258. data/lib/plutonium/ui/form/query.rb +8 -8
  259. data/lib/plutonium/ui/form/resource.rb +54 -26
  260. data/lib/plutonium/ui/frame_navigator_panel.rb +4 -4
  261. data/lib/plutonium/ui/grid/card.rb +3 -3
  262. data/lib/plutonium/ui/grid/resource.rb +3 -3
  263. data/lib/plutonium/ui/interaction/async/run_progress.rb +6 -6
  264. data/lib/plutonium/ui/interaction/async/running_banner.rb +2 -2
  265. data/lib/plutonium/ui/kanban/column.rb +6 -6
  266. data/lib/plutonium/ui/layout/base.rb +19 -1
  267. data/lib/plutonium/ui/layout/header.rb +1 -1
  268. data/lib/plutonium/ui/layout/icon_rail.rb +4 -4
  269. data/lib/plutonium/ui/layout/rodauth_layout.rb +1 -1
  270. data/lib/plutonium/ui/layout/sidebar.rb +1 -1
  271. data/lib/plutonium/ui/layout/topbar.rb +2 -2
  272. data/lib/plutonium/ui/modal/base.rb +4 -4
  273. data/lib/plutonium/ui/nav_grid_menu.rb +1 -1
  274. data/lib/plutonium/ui/nav_user.rb +2 -2
  275. data/lib/plutonium/ui/page/base.rb +10 -2
  276. data/lib/plutonium/ui/page/dashboard.rb +54 -0
  277. data/lib/plutonium/ui/page/edit.rb +3 -3
  278. data/lib/plutonium/ui/page/interactive_action.rb +2 -2
  279. data/lib/plutonium/ui/page/new.rb +3 -3
  280. data/lib/plutonium/ui/page/show.rb +2 -2
  281. data/lib/plutonium/ui/page/wizard.rb +22 -16
  282. data/lib/plutonium/ui/page/wizard_chooser.rb +10 -10
  283. data/lib/plutonium/ui/page/wizard_completed.rb +2 -2
  284. data/lib/plutonium/ui/skeleton_table.rb +1 -1
  285. data/lib/plutonium/ui/table/base.rb +2 -2
  286. data/lib/plutonium/ui/table/components/attachment.rb +1 -1
  287. data/lib/plutonium/ui/table/components/bulk_actions_toolbar.rb +19 -3
  288. data/lib/plutonium/ui/table/components/drag_handle.rb +3 -3
  289. data/lib/plutonium/ui/table/components/filter_form.rb +11 -10
  290. data/lib/plutonium/ui/table/components/filter_pills.rb +4 -4
  291. data/lib/plutonium/ui/table/components/pagy_info.rb +61 -17
  292. data/lib/plutonium/ui/table/components/pagy_pagination.rb +9 -5
  293. data/lib/plutonium/ui/table/components/row_actions_dropdown.rb +1 -1
  294. data/lib/plutonium/ui/table/components/scopes_bar.rb +6 -2
  295. data/lib/plutonium/ui/table/components/scopes_pills.rb +3 -3
  296. data/lib/plutonium/ui/table/components/selection_column.rb +1 -1
  297. data/lib/plutonium/ui/table/components/toolbar.rb +2 -2
  298. data/lib/plutonium/ui/table/components/view_switcher.rb +12 -8
  299. data/lib/plutonium/ui/table/resource.rb +53 -17
  300. data/lib/plutonium/ui/wizard/review.rb +7 -7
  301. data/lib/plutonium/ui/wizard/stepper.rb +1 -1
  302. data/lib/plutonium/ui/wizard/summary_display.rb +6 -0
  303. data/lib/plutonium/version.rb +1 -1
  304. data/lib/plutonium/wizard/base.rb +2 -0
  305. data/lib/plutonium/wizard/driving.rb +8 -2
  306. data/lib/plutonium/wizard/dsl.rb +4 -2
  307. data/lib/plutonium/wizard/field_importer.rb +1 -8
  308. data/lib/plutonium/wizard/review_step.rb +8 -1
  309. data/lib/plutonium/wizard/step.rb +14 -3
  310. data/package.json +4 -2
  311. data/plutonium.gemspec +4 -1
  312. data/postcss-gem-import.cjs +34 -0
  313. data/postcss-gem-import.js +3 -28
  314. data/src/css/components.css +101 -10
  315. data/src/css/slim_select.css +0 -38
  316. data/src/css/tokens.css +24 -0
  317. data/src/js/controllers/attachment_input_controller.js +15 -6
  318. data/src/js/controllers/chart_controller.js +162 -0
  319. data/src/js/controllers/clipboard_controller.js +3 -2
  320. data/src/js/controllers/dirty_form_guard_controller.js +4 -3
  321. data/src/js/controllers/easymde_controller.js +3 -0
  322. data/src/js/controllers/filter_panel_controller.js +15 -6
  323. data/src/js/controllers/flatpickr_controller.js +8 -0
  324. data/src/js/controllers/frame_refresh_controller.js +59 -0
  325. data/src/js/controllers/intl_tel_input_controller.js +5 -0
  326. data/src/js/controllers/register_controllers.js +4 -0
  327. data/src/js/controllers/slim_select_controller.js +79 -176
  328. data/src/js/core.js +9 -1
  329. data/src/js/i18n.js +69 -0
  330. data/src/js/plutonium-charts.js +15 -0
  331. data/src/js/turbo/turbo_confirm.js +5 -3
  332. metadata +64 -16
  333. data/.yarnrc.yml +0 -8
  334. data/config/initializers/hotwire_turbo_monkey_patches.rb +0 -12
  335. data/lib/generators/pu/field/input/input_generator.rb +0 -32
  336. data/lib/generators/pu/field/input/templates/input.rb.tt +0 -15
  337. data/lib/generators/pu/lib/plutonium_generators/concerns/config.rb +0 -39
  338. data/lib/generators/pu/lib/plutonium_generators/concerns/serializer.rb +0 -39
  339. data/lib/generators/pu/lib/plutonium_generators/installer.rb +0 -205
  340. data/lib/generators/pu/res/scaffold/templates/presenter.rb.tt +0 -13
  341. data/lib/generators/pu/res/scaffold/templates/query_object.rb.tt +0 -17
  342. data/public/plutonium-assets/application.js +0 -31419
  343. data/public/plutonium-assets/plutonium.ico +0 -0
  344. data/yarn.lock +0 -6858
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6abf981b5e375a2c576de8bfe5fe6448b115753356b6c91f1c3b0ae946e66935
4
- data.tar.gz: b519f55b96e20f592baab81b34a633de562dcd8060dbe2bf65cbff7342ae76dd
3
+ metadata.gz: b131f7d0e5b02982f4696e7d1db781f45b17f8b28f17da5fe5cc3b0a4480c7a0
4
+ data.tar.gz: f700d3d65a3fc368e73315c436a7f8d2afb5244cd04b4b0be5618efbc738d26f
5
5
  SHA512:
6
- metadata.gz: cbf194184d5d16439031b3802f5fc7b05b7f22b24a2075cac2d03a5e6869160f5332ea6cc4d83d3a580169db7405266435e5f9b7f4f1b42befd334a93a470f53
7
- data.tar.gz: ec740ece535648156d103872cb0362347b6b2f2823d425aa0b94134f336a32355c807f767d05a673f173670fb1efd572c42240542b48b44f0ab5dc8b23ed63cc
6
+ metadata.gz: 7dc926b3cf2fb69f5dfa4644d10c47e88a39e74f3359be2572a4ea6ea46ad216a5c0447959b0e1596f0ee6ae67190140f46acc53afa332a508e63f275440bdad
7
+ data.tar.gz: 4d9f3fba2b94d24ef0db3d5b24b35b30d6c058acc46ac20107d37c1a507f804dac7d47b3a38556f02b16ac229ce9b2a4140e73216152c9e56ae7189a5c71bc00
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: plutonium
3
- description: Use BEFORE starting any Plutonium work — new app, new feature, or first edit in an unfamiliar area. Routes you to the right skill and bootstraps greenfield work.
3
+ description: 'Use BEFORE starting any Plutonium work: new app, new feature, or first edit in an unfamiliar area. Routes you to the right skill and bootstraps greenfield work.'
4
4
  ---
5
5
 
6
- # Plutonium — Router & Bootstrapper
6
+ # Plutonium: Router & Bootstrapper
7
7
 
8
8
  Entry point for all Plutonium work. Does three things:
9
9
 
@@ -14,57 +14,57 @@ Entry point for all Plutonium work. Does three things:
14
14
  ## 🚨 Critical (read first)
15
15
 
16
16
  - **Plutonium is generator-driven.** Almost every file you'd hand-write has a `pu:*` generator. Hand-written files drift from conventions and break future generator runs.
17
- - **For greenfield** (new app, substantial new feature, first resource in a new domain) — load the **bootstrap bundle** below before writing code.
18
- - **For targeted edits** — use the **router table** to jump to the right skill.
19
- - **For anything touching tenant scoping** — load `plutonium-tenancy`. Don't reach for `where(organization: ...)` in a policy; fix the model instead.
17
+ - **For greenfield** (new app, substantial new feature, first resource in a new domain), load the **bootstrap bundle** below before writing code.
18
+ - **For targeted edits**: use the **router table** to jump to the right skill.
19
+ - **For anything touching tenant scoping**: load `plutonium-tenancy`. Don't reach for `where(organization: ...)` in a policy; fix the model instead.
20
20
  - **Unattended execution:** always pass `--dest=`, `--force` (when re-running meta-generators), `--auth=`, `--skip-bundle`, `--quiet` so generators don't block on prompts. See [Unattended execution](#unattended-execution).
21
- - **Inspect before you act.** Every targeted skill now opens with a CHECK gate — read the relevant files yourself before scaffolding or editing. Don't ask the user to describe their app when you can read it.
21
+ - **Inspect before you act.** Every targeted skill now opens with a CHECK gate: read the relevant files yourself before scaffolding or editing. Don't ask the user to describe their app when you can read it.
22
22
 
23
- ## The mental model (read once — it decides what you should write)
23
+ ## The mental model (read once: it decides what you should write)
24
24
 
25
- Plutonium applies Rails' bargain — follow the convention and the framework carries you; reach for an escape hatch when you need one — to the layer **above CRUD**: auth, authorization, multi-tenancy, admin UI, business operations. Four consequences change what you should actually type.
25
+ Plutonium applies Rails' bargain (follow the convention and the framework carries you; reach for an escape hatch when you need one) to the layer **above CRUD**: auth, authorization, multi-tenancy, admin UI, business operations. Four consequences change what you should actually type.
26
26
 
27
27
  ### 1. Everything is derived from something you already declared
28
28
 
29
- Not "defaults someone picked for you" — **computed from existing declarations**:
29
+ Not "defaults someone picked for you": **computed from existing declarations**:
30
30
 
31
31
  | Derived | From |
32
32
  |---|---|
33
33
  | Field types, required markers, select choices | model columns, associations, attachments, enums, **and validations** (`presence: true` → required; `inclusion:` → select choices) |
34
- | A collection's preloads (index, kanban, export) | the policy's permitted field set — there is **no `includes` list to write or maintain** |
35
- | Tenant scope | your associations — direct `belongs_to`, then `has_one`/`has_one :through`, then reverse `has_many` |
34
+ | A collection's preloads (index, kanban, export) | the policy's permitted field set; there is **no `includes` list to write or maintain** |
35
+ | Tenant scope | your associations: direct `belongs_to`, then `has_one`/`has_one :through`, then reverse `has_many` |
36
36
  | Action type (record / bulk / resource) | whether the interaction declares `:resource`, `:resources`, or neither |
37
37
  | An association input's typeahead | the **target resource's own `search` block** |
38
38
  | CRUD, nested and action routes | one `register_resource` line |
39
39
 
40
- ⇒ **Declare only what differs.** A `field :title` matching the detected type is dead code — and one more line to fall out of step when the column changes. This is the single most common way generated-looking code goes wrong.
40
+ ⇒ **Declare only what differs.** A `field :title` matching the detected type is dead code, and one more line to fall out of step when the column changes. This is the single most common way generated-looking code goes wrong.
41
41
 
42
42
  ### 2. Definition and policy answer different questions
43
43
 
44
44
  - **Definition** = *how* a field renders.
45
45
  - **Policy** = *whether it appears at all*.
46
46
 
47
- "Only admins see this field" is `permitted_attributes_for_*`. Never a definition declaration, and never a `condition:` (that only hides UI — the route stays live).
47
+ "Only admins see this field" is `permitted_attributes_for_*`. Never a definition declaration, and never a `condition:` (that only hides UI; the route stays live).
48
48
 
49
49
  ### 3. Overrides are plain Ruby inheritance
50
50
 
51
- `AdminPortal::PostDefinition < ::PostDefinition`, and the same for policies and controllers. App-level default, portal-level subclass. No registry of overrides, no precedence DSL, no merge semantics — so "why does this field show here but not there" is always readable as a class hierarchy.
51
+ `AdminPortal::PostDefinition < ::PostDefinition`, and the same for policies and controllers. App-level default, portal-level subclass. No registry of overrides, no precedence DSL, no merge semantics, so "why does this field show here but not there" is always readable as a class hierarchy.
52
52
 
53
53
  ### 4. Climb the escape-hatch ladder only as far as the problem requires
54
54
 
55
- 1. **Change an option** — `input :content, as: :markdown`
56
- 2. **Render inline** — `display :priority, as: :phlexi_render, with: ->(value, attrs) do … end`
57
- 3. **Write a component** — a *field* component (subclasses the Phlexi base) plugs into `as:`; anything with its own constructor goes through a block (`display :card do |field| … end`)
58
- 4. **Implement a hook** — controller hooks instead of reopening `create`/`update`; page `render_before_*` / `render_after_*` instead of `view_template`
59
- 5. **Replace the page** — `view_template` on the nested class, or an ERB view at the controller path (ERB wins when both exist)
55
+ 1. **Change an option**: `input :content, as: :markdown`
56
+ 2. **Render inline**: `display :priority do |f| … end` (a block, `instance_exec`ed in Phlex, emits markup directly and gives you `f.object`)
57
+ 3. **Write a component**: a *field* component (subclasses the Phlexi base) plugs into `as:`; anything with its own constructor goes through a block (`display :card do |field| … end`)
58
+ 4. **Implement a hook**: controller hooks instead of reopening `create`/`update`; page `render_before_*` / `render_after_*` instead of `view_template`
59
+ 5. **Replace the page**: `view_template` on the nested class, or an ERB view at the controller path (ERB wins when both exist)
60
60
 
61
61
  Reaching for rung 5 on a rung-1 problem is how you end up owning breadcrumbs, the header and turbo frame wiring you never meant to touch.
62
62
 
63
63
  **Underneath all of it, it stays Rails.** Models are plain ActiveRecord, controllers inherit from Rails controllers, views resolve through Rails view paths. A Plutonium resource and a hand-written controller coexist in one app.
64
64
 
65
- ## ✅ Orient before you route (CHECK — read the app, don't assume)
65
+ ## ✅ Orient before you route (CHECK: read the app, don't assume)
66
66
 
67
- A one-line request rarely says whether this is a new app, a half-built one, or a multi-tenant one — and those change which path you take. Spend 30 seconds reading the app **before** loading a bundle or running anything:
67
+ A one-line request rarely says whether this is a new app, a half-built one, or a multi-tenant one, and those change which path you take. Spend 30 seconds reading the app **before** loading a bundle or running anything:
68
68
 
69
69
  | Read | Tells you |
70
70
  |---|---|
@@ -80,15 +80,16 @@ This is the global "look before you leap"; each targeted skill carries its own A
80
80
  | Skill | Covers |
81
81
  |---|---|
82
82
  | **[[plutonium-app]]** | Installation, packages (feature + portal), portal engines, mounting, `register_resource` (including singular and custom routes), `pu:res:conn` |
83
- | **[[plutonium-resource]]** | The resource itself — `pu:res:scaffold`, field types, model layer (`Plutonium::Resource::Record`, `has_cents`, SGID, routing), definition layer (fields/inputs/displays/columns, search/filters/scopes/sorting, custom actions, bulk actions, index views, page customization) |
83
+ | **[[plutonium-resource]]** | The resource itself: `pu:res:scaffold`, field types, model layer (`Plutonium::Resource::Record`, `has_cents`, SGID, routing), definition layer (fields/inputs/displays/columns, search/filters/scopes/sorting, custom actions, bulk actions, index views, page customization) |
84
84
  | **[[plutonium-behavior]]** | Controllers (hooks, key methods, presentation), policies (action methods, `permitted_attributes_for_*`, `permitted_associations`), interactions (structure, outcomes, chaining, URL generation) |
85
- | **[[plutonium-async-interactions]]** | Async interactions — `async`, the Run STI model, failure policies (`halt`/`continue`/`transactional`), authorization re-derivation at perform time, registering AsyncRun as a resource (progress page + running banner), scheduling `ReapJob` |
85
+ | **[[plutonium-async-interactions]]** | Async interactions: `async`, the Run STI model, failure policies (`halt`/`continue`/`transactional`), authorization re-derivation at perform time, registering AsyncRun as a resource (progress page + running banner), scheduling `ReapJob` |
86
86
  | **[[plutonium-ui]]** | Page classes, forms, displays, tables, custom Phlex components, layouts, modals & tabs, Tailwind config, Stimulus, design tokens, `.pu-*` classes, Phlexi themes |
87
- | **[[plutonium-kanban]]** | `kanban do…end` DSL in a Definition — columns, `card_fields`, `position_on`, `realtime`, column actions, `kanban_move?` policy, quick-add, static vs dynamic boards |
87
+ | **[[plutonium-kanban]]** | `kanban do…end` DSL in a Definition, columns, `card_fields`, `position_on`, `realtime`, column actions, `kanban_move?` policy, quick-add, static vs dynamic boards |
88
+ | **[[plutonium-dashboard]]** | Dashboards, `Plutonium::Dashboard::Base`, `metric` / `chart` / `card`, `register_dashboard`, lazy turbo-frame cards, `refresh`, `condition:`, `authorize?`, `pu:dashboard` |
88
89
  | **[[plutonium-auth]]** | Rodauth install, account types (basic / admin / SaaS), profile resource, security section |
89
90
  | **[[plutonium-tenancy]]** | Entity scoping (`associated_with`, `default_relation_scope`, three model shapes), nested resources, invites |
90
91
  | **[[plutonium-testing]]** | `pu:test:install`, `pu:test:scaffold`, `ResourceCrud`/`ResourcePolicy`/`ResourceDefinition`/`ResourceModel`/`NestedResource`/`PortalAccess`/`ResourceInteraction`, `AuthHelpers` |
91
- | **[[plutonium-wizard]]** | Multi-step flows — the wizard DSL (`step`/`review`/`using:`/`condition:`, per-step `on_submit`/`persist`/`on_rollback`, `execute`), anchoring & resume, one-time wizards + gate, registration (`wizard` macro + `register_wizard`), storage/config + SweepJob |
92
+ | **[[plutonium-wizard]]** | Multi-step flows: the wizard DSL (`step`/`review`/`using:`/`condition:`, per-step `on_submit`/`persist`/`on_rollback`, `execute`), anchoring & resume, one-time wizards + gate, registration (`wizard` macro + `register_wizard`), storage/config + SweepJob |
92
93
 
93
94
  ## Greenfield bootstrap bundle
94
95
 
@@ -96,10 +97,10 @@ Triggers: installing Plutonium, building a new app, adding the first resource in
96
97
 
97
98
  **Load these before writing code:**
98
99
 
99
- 1. **`plutonium-app`** — install, portals, packages, routes.
100
- 2. **`plutonium-resource`** — scaffold, model, definition (the bulk of the work).
101
- 3. **`plutonium-behavior`** — controllers, policies, interactions.
102
- 4. **`plutonium-tenancy`** — only if multi-tenant; load before declaring entity scoping.
100
+ 1. **`plutonium-app`**: install, portals, packages, routes.
101
+ 2. **`plutonium-resource`**: scaffold, model, definition (the bulk of the work).
102
+ 3. **`plutonium-behavior`**: controllers, policies, interactions.
103
+ 4. **`plutonium-tenancy`**: only if multi-tenant; load before declaring entity scoping.
103
104
 
104
105
  Add when relevant:
105
106
  - **`plutonium-auth`** for login / accounts / profile.
@@ -113,7 +114,7 @@ Add when relevant:
113
114
  | Install Plutonium, create a portal or package, mount engines, register routes (incl. singular / custom routes) | **[[plutonium-app]]** |
114
115
  | Run `pu:res:scaffold`, pick field types, set scaffold options | **[[plutonium-resource]]** |
115
116
  | Edit a model, add associations, use `has_cents`, override `to_param` / `to_label` | **[[plutonium-resource]]** |
116
- | Edit a definition — fields, inputs, displays, columns, search, filters, scopes, custom actions, bulk actions, index views, modal/slideover, page titles | **[[plutonium-resource]]** |
117
+ | Edit a definition, fields, inputs, displays, columns, search, filters, scopes, custom actions, bulk actions, index views, modal/slideover, page titles | **[[plutonium-resource]]** |
117
118
  | Override a controller action, hook, redirect, or `resource_params` | **[[plutonium-behavior]]** |
118
119
  | Write `relation_scope`, `permitted_attributes_for_*`, `permitted_associations`, action methods, or any policy override | **[[plutonium-behavior]]** (+ **[[plutonium-tenancy]]** if scoping) |
119
120
  | Write an interaction class for business logic | **[[plutonium-behavior]]** |
@@ -121,23 +122,24 @@ Add when relevant:
121
122
  | Scope a model to a tenant, write `associated_with`, set portal entity strategy | **[[plutonium-tenancy]]** |
122
123
  | Configure parent/child nested routes, custom parent resolution | **[[plutonium-tenancy]]** |
123
124
  | Set up user invitations or entity membership | **[[plutonium-tenancy]]** |
124
- | Build or customize a kanban board view — `kanban do…end`, columns, `card_fields`, `position_on`, `realtime`, column actions, `kanban_move?` policy | **[[plutonium-kanban]]** |
125
+ | Build or customize a kanban board view, `kanban do…end`, columns, `card_fields`, `position_on`, `realtime`, column actions, `kanban_move?` policy | **[[plutonium-kanban]]** |
126
+ | Build a dashboard, KPI overview or chart page, `pu:dashboard`, `metric` / `chart` / `card`, `register_dashboard`, refresh, per-card conditions | **[[plutonium-dashboard]]** |
125
127
  | Build a custom page (override `ShowPage`/`IndexPage`/`NewPage`/`EditPage`), custom form, custom display, custom table, custom Phlex component | **[[plutonium-ui]]** |
126
128
  | Configure Tailwind, register Stimulus controllers, edit design tokens, theme forms/displays/tables, write a custom layout | **[[plutonium-ui]]** |
127
129
  | Install Rodauth, set up accounts, configure login flow, add the profile resource | **[[plutonium-auth]]** |
128
130
  | Write tests for a resource, run `pu:test:scaffold`, include `Plutonium::Testing::*` concerns | **[[plutonium-testing]]** |
129
- | Build a multi-step flow — onboarding, checkout, branching create — register a `wizard` / `register_wizard`, gate a one-time wizard | **[[plutonium-wizard]]** |
131
+ | Build a multi-step flow, onboarding, checkout, branching create, register a `wizard` / `register_wizard`, gate a one-time wizard | **[[plutonium-wizard]]** |
130
132
 
131
133
  ## Resource architecture at a glance
132
134
 
133
- A **resource** is four cooperating layers — Plutonium auto-fills defaults from the model, so you only declare overrides:
135
+ A **resource** is four cooperating layers; Plutonium auto-fills defaults from the model, so you only declare overrides:
134
136
 
135
137
  | Layer | File | Purpose |
136
138
  |---|---|---|
137
139
  | **Model** | `app/models/post.rb` | Data, validations, associations |
138
- | **Definition** | `app/definitions/post_definition.rb` | UI — fields, filters, actions |
139
- | **Policy** | `app/policies/post_policy.rb` | Authorization — who, what |
140
- | **Controller** | `app/controllers/posts_controller.rb` | Request handling (rarely edited — use hooks) |
140
+ | **Definition** | `app/definitions/post_definition.rb` | UI, fields, filters, actions |
141
+ | **Policy** | `app/policies/post_policy.rb` | Authorization: who, what |
142
+ | **Controller** | `app/controllers/posts_controller.rb` | Request handling (rarely edited; use hooks) |
141
143
 
142
144
  Plus one optional fifth layer:
143
145
 
@@ -169,6 +171,7 @@ Every Plutonium generator is discoverable via `rails g pu:<tab>`. Always pass `-
169
171
  | `pu:eject:shell` | Eject topbar/sidebar partials | `plutonium-ui` |
170
172
  | `pu:test:install` | Install `Plutonium::Testing` scaffolding | `plutonium-testing` |
171
173
  | `pu:test:scaffold NAME --portals=...` | Scaffold integration tests | `plutonium-testing` |
174
+ | `pu:dashboard NAME --dest=PORTAL [--at=/]` | Dashboard class + `register_dashboard` route | `plutonium-dashboard` |
172
175
  | `pu:skills:sync` | Sync Plutonium Claude skills into the project | (this skill) |
173
176
 
174
177
  ## Unattended execution
@@ -188,8 +191,8 @@ Meta-generators (`pu:saas:setup`) propagate flags to the generators they chain.
188
191
  ## Workflow summary
189
192
 
190
193
  1. **Load the bootstrap bundle** (or the targeted skill from the router table).
191
- 2. **Generate** — `rails g pu:res:scaffold Model field:type ... --dest=main_app`.
192
- 3. **Migrate** — `rails db:prepare`.
193
- 4. **Connect** — `rails g pu:res:conn Model --dest=portal_name`.
194
- 5. **Customize** — edit definition / policy as needed.
195
- 6. **Verify** — hit the route in the browser.
194
+ 2. **Generate**: `rails g pu:res:scaffold Model field:type ... --dest=main_app`.
195
+ 3. **Migrate**: `rails db:prepare`.
196
+ 4. **Connect**: `rails g pu:res:conn Model --dest=portal_name`.
197
+ 5. **Customize**: edit definition / policy as needed.
198
+ 6. **Verify**: hit the route in the browser.
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: plutonium-app
3
- description: Use BEFORE installing Plutonium, creating a portal or feature package, mounting an engine, or registering resources/routes. Covers initial setup, the package system, portal engines, route registration (including singular and custom routes), and resource-to-portal wiring.
3
+ description: 'Use BEFORE installing Plutonium, creating a portal or feature package, mounting an engine, or registering resources/routes. Covers initial setup, the package system, portal engines, route registration (including singular and custom routes), and resource-to-portal wiring.'
4
4
  ---
5
5
 
6
- # Plutonium App — Installation, Packages, Portals, Routes
6
+ # Plutonium App: Installation, Packages, Portals, Routes
7
7
 
8
8
  How a Plutonium app is assembled: the install bootstrap, the package system (feature vs portal), portal engines, and the routing surface that exposes resources to the web.
9
9
 
@@ -15,54 +15,54 @@ For the resources themselves (model + definition + scaffold options), see [[plut
15
15
  - **Existing app → `base.rb`. New app → `plutonium.rb`.** The `plutonium.rb` template re-runs full bootstrap (dotenv, annotate, solid_*, asset config) and creates generic "initial commit" commits that clobber history. For any pre-existing app use `base.rb`.
16
16
  - **Pass `--dest`, `--auth`, `--force`, `--skip-bundle`** etc. for unattended runs so generators don't block on prompts.
17
17
  - **Feature vs portal is a hard split.** Feature packages hold models/policies/definitions/interactions. Portal packages hold controllers/views/routes/auth. Don't mix.
18
- - **Package classes are auto-namespaced** — `packages/blogging/app/models/blogging/post.rb` → `Blogging::Post`. Don't fight it.
19
- - **Always connect resources with `pu:res:conn`** — until connected, a resource has no portal routes and is invisible.
20
- - **For custom routes on a registered resource, pass `as:`** — otherwise `resource_url_for` can't build URLs.
18
+ - **Package classes are auto-namespaced**: `packages/blogging/app/models/blogging/post.rb` → `Blogging::Post`. Don't fight it.
19
+ - **Always connect resources with `pu:res:conn`**: until connected, a resource has no portal routes and is invisible.
20
+ - **For custom routes on a registered resource, pass `as:`**: otherwise `resource_url_for` can't build URLs.
21
21
 
22
22
  ---
23
23
 
24
- ## 🛑 Before you install or scaffold structure: confirm the shape (ASK — don't infer)
24
+ ## 🛑 Before you install or scaffold structure: confirm the shape (ASK: don't infer)
25
25
 
26
- "Set up Plutonium" / "make an admin area" / "create a billing package" each hide a high-blast-radius decision. Get one wrong and you **clobber a year of git history**, build the wrong kind of package, or ship a portal nobody can log into. Resolve each — confirming by inspection (next section), not assumption:
26
+ "Set up Plutonium" / "make an admin area" / "create a billing package" each hide a high-blast-radius decision. Get one wrong and you **clobber a year of git history**, build the wrong kind of package, or ship a portal nobody can log into. Resolve each, confirming by inspection (next section), not assumption:
27
27
 
28
- 1. **Fresh app or existing one?** Existing ⇒ `bundle add plutonium` + `pu:core:install` (the `base.rb` path). **NEVER the `plutonium.rb` fresh-app template on an existing app** — it re-bootstraps (dotenv/annotate/solid_*/assets) and drops "initial commit" commits that clobber history. This is the single most dangerous mistake in this skill — confirm it's greenfield *before* reaching for `plutonium.rb`.
29
- 2. **Feature package or portal package?** Business logic (models/policies/definitions/interactions) ⇒ `pu:pkg:package` (feature, no UI). A web surface (controllers/views/routes/auth) ⇒ `pu:pkg:portal`. Hard split — "billing" is a *feature*; "admin area" is a *portal*. A feature package is invisible until its resources are `pu:res:conn`'d into a portal.
30
- 3. **Auth per portal.** `--auth=<account>` / `--public` / `--byo` / `--scope=<Entity>` (multi-tenant). Unguessable from "admin area" — decide, don't default silently.
31
- 4. **Don't stop half-wired.** A resource reaches the browser only after: scaffold → migrate → `pu:res:conn --dest=portal` → portal engine `mount`ed in `config/routes.rb` → registered (conn does the last). Name the whole chain before you start.
28
+ 1. **Fresh app or existing one?** Existing ⇒ `bundle add plutonium` + `pu:core:install` (the `base.rb` path). **NEVER the `plutonium.rb` fresh-app template on an existing app**: it re-bootstraps (dotenv/annotate/solid_*/assets) and drops "initial commit" commits that clobber history. This is the single most dangerous mistake in this skill; confirm it's greenfield *before* reaching for `plutonium.rb`.
29
+ 2. **Feature package or portal package?** Business logic (models/policies/definitions/interactions) ⇒ `pu:pkg:package` (feature, no UI). A web surface (controllers/views/routes/auth) ⇒ `pu:pkg:portal`. Hard split: "billing" is a *feature*; "admin area" is a *portal*. A feature package is invisible until its resources are `pu:res:conn`'d into a portal.
30
+ 3. **Auth per portal.** `--auth=<account>` / `--public` / `--byo` / `--scope=<Entity>` (multi-tenant). Unguessable from "admin area"; decide, don't default silently.
31
+ 4. **Don't stop half-wired.** A resource reaches the browser only after: scaffold → migrate → `pu:res:conn --dest=portal` → registered in the portal's routes (conn does this) → portal engine mounted (`pu:pkg:portal` already did this). Name the whole chain before you start.
32
32
 
33
- **Never ship a guessed schema, portal name, or auth flag as applied commands** — read them off the app first; fall back to `AskUserQuestion` only for genuine product choices (separate staff accounts vs shared, which payment backend). The decisions compound: *existing app ⇒ base.rb path*; *feature package ⇒ needs a portal to be visible*; *new portal ⇒ pick auth + mount it*.
33
+ **Never ship a guessed schema, portal name, or auth flag as applied commands**; read them off the app first; fall back to `AskUserQuestion` only for genuine product choices (separate staff accounts vs shared, which payment backend). The decisions compound: *existing app ⇒ base.rb path*; *feature package ⇒ needs a portal to be visible*; *new portal ⇒ pick auth + mount it*.
34
34
 
35
- ## ✅ Before you run a generator: verify the ground truth (CHECK — read it, don't ask for it)
35
+ ## ✅ Before you run a generator: verify the ground truth (CHECK: read it, don't ask for it)
36
36
 
37
- You have file access — **inspect**; don't ask the user to describe their app.
37
+ You have file access, **inspect**; don't ask the user to describe their app.
38
38
 
39
39
  | Check | How | Why it matters |
40
40
  |---|---|---|
41
- | Greenfield vs existing | `git log --oneline \| head`; is there a populated `Gemfile`/`app/`? | An existing app must use the `base.rb` path — **never** `plutonium.rb` |
41
+ | Greenfield vs existing | `git log --oneline \| head`; is there a populated `Gemfile`/`app/`? | An existing app must use the `base.rb` path, **never** `plutonium.rb` |
42
42
  | Plutonium already installed | grep `Gemfile` for `plutonium`; `ls config/packages.rb app/controllers/resource_controller.rb` | Avoid re-installing / double bootstrap |
43
- | Package/portal already exists | `ls packages/<name>` | Don't duplicate — connect to / extend the existing one |
43
+ | Package/portal already exists | `ls packages/<name>` | Don't duplicate; connect to / extend the existing one |
44
44
  | Existing auth | grep `Gemfile`/`app/models` for `rodauth`/`devise`/`has_secure_password` | Drives `--auth` vs `--byo` |
45
- | Portal engine mounted | grep `config/routes.rb` for `mount <Portal>::Engine` | An unmounted portal 404s |
45
+ | Portal engine mounted | grep `packages/<portal>/config/routes.rb` for `mount <Portal>::Engine` | `pu:pkg:portal` writes the mount there. Don't add a second one to `config/routes.rb` (duplicate route name error) |
46
46
  | Resource registered | grep the portal's `config/routes.rb` for `register_resource ::<X>` | Unregistered ⇒ no URLs (`resource_url_for` fails) |
47
47
  | Migrations applied | `rails db:migrate:status` before `pu:res:conn` | `conn` seeds the policy from columns |
48
48
 
49
49
  Inspect with your own tools **before** running any generator.
50
50
 
51
- ## 🛠 Use the generator — pick the right install path
51
+ ## 🛠 Use the generator: pick the right install path
52
52
 
53
53
  Never hand-write base controllers, engine files, layouts, or route registration. Pass `--dest`/`--auth`/`--force`/`--skip-bundle` for unattended runs.
54
54
 
55
55
  | Task | Generator | Verify first |
56
56
  |---|---|---|
57
- | Install — **existing** app | `bundle add plutonium` + `pu:core:install` | It's an existing app (use `base.rb`, **not** `plutonium.rb`) |
58
- | Install — **fresh** app | `rails new … -m …/plutonium.rb` | Brand-new app **only** |
57
+ | Install, **existing** app | `bundle add plutonium` + `pu:core:install` | It's an existing app (use `base.rb`, **not** `plutonium.rb`) |
58
+ | Install, **fresh** app | `rails new … -m …/plutonium.rb` | Brand-new app **only** |
59
59
  | Feature package | `pu:pkg:package <name>` | Not already present |
60
- | Portal package | `pu:pkg:portal <name> --auth=…/--public/--byo/--scope=…` | Auth strategy decided; then `mount` the engine by hand |
60
+ | Portal package | `pu:pkg:portal <name> --auth=…/--public/--byo/--scope=…` | Auth strategy decided. The generator also mounts the engine (see Mounting) |
61
61
  | Connect a resource | `pu:res:conn <Res> --dest=portal` | Migrated; target portal exists |
62
62
 
63
63
  ---
64
64
 
65
- # Part 1 — Installation
65
+ # Part 1: Installation
66
66
 
67
67
  ## Fresh Rails app (recommended)
68
68
 
@@ -78,11 +78,11 @@ Configures Rails + Propshaft + esbuild + TailwindCSS + Plutonium in one shot.
78
78
  ⚠️ Use `base.rb`, **not** `plutonium.rb`.
79
79
 
80
80
  ```bash
81
- # Option 1 — template
81
+ # Option 1: template
82
82
  bin/rails app:template \
83
83
  LOCATION=https://radioactive-labs.github.io/plutonium-core/templates/base.rb
84
84
 
85
- # Option 2 — manual
85
+ # Option 2: manual
86
86
  # Add `gem "plutonium"` to Gemfile, then:
87
87
  bundle install
88
88
  rails generate pu:core:install
@@ -108,10 +108,7 @@ rails db:prepare
108
108
  # 5. Connect resource to portal
109
109
  rails generate pu:res:conn Post --dest=admin_portal
110
110
 
111
- # 6. Mount portal in config/routes.rb
112
- # mount AdminPortal::Engine, at: "/admin"
113
-
114
- # 7. Start
111
+ # 6. Start (step 3 already mounted the portal at /admin)
115
112
  rails server
116
113
  ```
117
114
 
@@ -121,10 +118,10 @@ rails server
121
118
  app/
122
119
  ├── controllers/
123
120
  │ ├── plutonium_controller.rb # non-resource base
124
- │ └── resource_controller.rb # CRUD base — see plutonium-behavior
121
+ │ └── resource_controller.rb # CRUD base; see plutonium-behavior
125
122
  ├── definitions/resource_definition.rb
126
123
  ├── interactions/resource_interaction.rb
127
- ├── models/resource_record.rb # abstract model — includes Plutonium::Resource::Record
124
+ ├── models/resource_record.rb # abstract model that includes Plutonium::Resource::Record
128
125
  ├── policies/resource_policy.rb
129
126
  └── views/layouts/resource.html.erb
130
127
 
@@ -174,7 +171,7 @@ end
174
171
 
175
172
  ---
176
173
 
177
- # Part 2 — The Package System
174
+ # Part 2: The Package System
178
175
 
179
176
  Two kinds, hard split:
180
177
 
@@ -263,21 +260,31 @@ end
263
260
 
264
261
  This is loaded from `config/application.rb`. Migrations from all packages are picked up by `rails db:migrate` automatically.
265
262
 
263
+ ## Locale files
264
+
265
+ Packages and portals are Rails engines, so `packages/<name>/config/locales/*.yml` loads automatically, ahead of the app's own `config/locales` (app wins, then packages, then the gem's defaults). Both generators scaffold `config/locales/en.yml`.
266
+
267
+ - Feature package: model/attribute names (`activerecord.models`, `activerecord.attributes`) and field text (`plutonium.fields.<model>.<attr>.{placeholder,hint,description}`).
268
+ - Portal: portal-specific wording under `plutonium.portals.<portal_namespace>.…` (same tree as the global keys: `fields`, `actions`, `scopes`, `filters`, `kanban_columns`, `values`). Wins over the global key only inside that portal.
269
+ - Any of Plutonium's own strings (`plutonium.*` in the gem's `config/locales/en/*.yml`) can be overridden from either place. Pagination text comes from Pagy's dictionaries instead.
270
+
271
+ Switch locale with a normal Rails `around_action` in the portal's controller concern; Plutonium reads `I18n.locale` and never sets it. See `docs/reference/i18n.md`.
272
+
266
273
  ## When to use which
267
274
 
268
- **Feature packages** — domain logic that:
275
+ **Feature packages** are domain logic that:
269
276
  - Could be reused across multiple portals (admin and customer both edit `Blogging::Post`)
270
277
  - Has no inherent UI / auth (it's just behavior)
271
278
  - You want to keep isolated from other domains (`billing` should not depend on `blogging`)
272
279
 
273
- **Portal packages** — user-facing surfaces that:
280
+ **Portal packages** are user-facing surfaces that:
274
281
  - Have a specific auth flow (admin vs customer vs public)
275
282
  - Render different views of the same underlying resources
276
283
  - Need different policies / definitions per audience
277
284
 
278
285
  ---
279
286
 
280
- # Part 3 — Portal Engines
287
+ # Part 3: Portal Engines
281
288
 
282
289
  A portal is a Rails engine mixing in `Plutonium::Portal::Engine`. It defines its own routes, controller concern, and (optionally) entity scoping.
283
290
 
@@ -292,7 +299,7 @@ rails g pu:pkg:portal <name>
292
299
  | Option | Description |
293
300
  |---|---|
294
301
  | `--auth=NAME` | Rodauth account to use (e.g. `--auth=user`) |
295
- | `--public` | Public access — no auth |
302
+ | `--public` | Public access, no auth |
296
303
  | `--byo` | Bring your own auth |
297
304
  | `--scope=CLASS` | Entity class for multi-tenancy (e.g. `--scope=Organization`) |
298
305
 
@@ -323,7 +330,9 @@ end
323
330
 
324
331
  ## Controller concern (auth)
325
332
 
326
- Every portal has a `Concerns::Controller` mixed into its `ResourceController`. The generator wires this up; you customize it for auth / before_action hooks.
333
+ Every portal has a `Concerns::Controller`, included by both its `ResourceController` (resource pages) and its `PlutoniumController` (dashboard and other non-resource pages). The generator wires this up; you customize it for auth / before_action hooks.
334
+
335
+ Portal-wide helpers the layout calls belong here, declared with `helper_method`, so they work on the dashboard as well as resource pages. Defining one on a single resource controller (or on the app's `::ResourceController`) leaves the dashboard without it. The common case is `profile_url`: `Plutonium::Auth::Rodauth` defines it as `nil`, and the avatar menu only shows a Profile link when it is truthy. `pu:profile:conn --dest=<portal>` writes the override into this concern; it reads `current_user.profile`, the `has_one :profile` association that `pu:profile:install` adds (see [[plutonium-auth]]). A plain `pu:res:conn --singular` registers the route but does not define `profile_url`. When testing the link, request both the portal root and a resource page.
327
336
 
328
337
  ### Rodauth
329
338
 
@@ -361,19 +370,62 @@ end
361
370
 
362
371
  ## Mounting
363
372
 
373
+ `pu:pkg:portal` writes the mount at the bottom of the portal's own `packages/<name>_portal/config/routes.rb`, at `/<name>`, wrapped in an auth constraint when `--auth` is given:
374
+
364
375
  ```ruby
365
- # config/routes.rb
376
+ # packages/admin_portal/config/routes.rb (after the engine's routes.draw block)
366
377
  Rails.application.routes.draw do
367
- # Authenticated mount
368
378
  constraints Rodauth::Rails.authenticate(:user) do
369
379
  mount AdminPortal::Engine, at: "/admin"
370
380
  end
381
+ end
382
+ ```
383
+
384
+ To change the path, edit `at:` in place. Don't mount the engine again in `config/routes.rb`: the second `mount` reuses the route name (`admin_portal`) and Rails raises `ArgumentError: Invalid route name, already in use`.
385
+
386
+ Route order matters: the app's `config/routes.rb` is drawn first, then each package's routes file, then gem engines (Active Storage, Turbo).
387
+
388
+ ### Mounting a portal at `/`
389
+
390
+ Two things change when a portal is mounted at `"/"`.
391
+
392
+ **Drop the `Rodauth::Rails.authenticate` constraint and authenticate in the controller concern instead.** The constraint does not fail the match for an anonymous visitor; it calls `rodauth.require_account`, which redirects to login. A constrained mount at `/` matches every path the app's own routes did not claim, so it redirects anonymous requests meant for routes drawn after it (other portals, Active Storage) and turns unknown URLs into login redirects.
393
+
394
+ ```ruby
395
+ # packages/desk_portal/config/routes.rb
396
+ Rails.application.routes.draw do
397
+ mount DeskPortal::Engine, at: "/"
398
+ end
399
+
400
+ # packages/desk_portal/app/controllers/desk_portal/concerns/controller.rb
401
+ module DeskPortal
402
+ module Concerns
403
+ module Controller
404
+ extend ActiveSupport::Concern
405
+ include Plutonium::Portal::Controller
406
+ include Plutonium::Auth::Rodauth(:user)
407
+ # add concerns above.
408
+
409
+ included do
410
+ before_action { rodauth.require_account }
411
+ end
412
+ end
413
+ end
414
+ end
415
+ ```
371
416
 
372
- # Unconstrained (portal handles its own auth)
373
- mount PublicPortal::Engine, at: "/public"
417
+ **Move the engine's root off `/` but keep the name.** The app's `root` is drawn first, so the generated `root to: "dashboard#index"` is unreachable and the portal's `root_path` points at the app's home page. Plutonium's header, icon rail, breadcrumbs and wizard exits all link to `root_path`, so the portal still needs a route named `root`:
418
+
419
+ ```ruby
420
+ DeskPortal::Engine.routes.draw do
421
+ get "dashboard", to: "dashboard#index", as: :root
422
+ register_resource ::Comment
423
+ # register resources above.
374
424
  end
375
425
  ```
376
426
 
427
+ The same applies to `register_dashboard ..., at: "/"`. Confirm with `Rails.application.routes.recognize_path("/")` (still the app's home) and `recognize_path("/dashboard")`.
428
+
377
429
  ## Controller hierarchy
378
430
 
379
431
  Portal controllers inherit from the feature-package controller if one exists, OR from the portal's `ResourceController` otherwise.
@@ -402,13 +454,13 @@ end
402
454
  ## Per-portal overrides
403
455
 
404
456
  ```ruby
405
- # Definition — how fields render (NOT whether they appear)
457
+ # Definition: how fields render (NOT whether they appear)
406
458
  class AdminPortal::PostDefinition < ::PostDefinition
407
459
  scope :pending_review
408
460
  input :internal_notes, hint: "Not shown to the author"
409
461
  end
410
462
 
411
- # Policy — whether a field appears at all
463
+ # Policy: whether a field appears at all
412
464
  class AdminPortal::PostPolicy < ::PostPolicy
413
465
  include AdminPortal::ResourcePolicy
414
466
  def destroy? = true
@@ -426,7 +478,7 @@ end
426
478
 
427
479
  ---
428
480
 
429
- # Part 4 — Routes & `register_resource`
481
+ # Part 4: Routes & `register_resource`
430
482
 
431
483
  Portal routes live in `packages/<name>_portal/config/routes.rb`:
432
484
 
@@ -442,7 +494,7 @@ AdminPortal::Engine.routes.draw do
442
494
  end
443
495
  ```
444
496
 
445
- ## `register_resource` — what it does
497
+ ## `register_resource`: what it does
446
498
 
447
499
  For each call, Plutonium auto-generates:
448
500
 
@@ -450,11 +502,11 @@ For each call, Plutonium auto-generates:
450
502
  - Nested routes for every registered `has_many` / `has_one` parent (prefixed `nested_`)
451
503
  - Route names that `resource_url_for` can resolve
452
504
 
453
- You list every resource the portal exposes. If a resource isn't registered, it has no URLs in that portal — `resource_url_for` will fail.
505
+ You list every resource the portal exposes. If a resource isn't registered, it has no URLs in that portal, so `resource_url_for` will fail.
454
506
 
455
507
  ## Singular (singleton) resources
456
508
 
457
- For resources with no collection — a single per-user `Profile`, app-wide `Settings`, etc.:
509
+ For resources with no collection, a single per-user `Profile`, app-wide `Settings`, etc.:
458
510
 
459
511
  ```ruby
460
512
  register_resource ::Profile, singular: true
@@ -475,6 +527,10 @@ Use the `--singular` flag on `pu:res:conn`:
475
527
  rails g pu:res:conn Profile --dest=customer_portal --singular
476
528
  ```
477
529
 
530
+ ## Dashboards
531
+
532
+ `register_dashboard SomeDashboard, at: "overview"` mounts a `Plutonium::Dashboard::Base` subclass (metric / chart / free-form cards, each in its own lazy turbo frame) on the portal at `/dashboards/overview`; `at: "/"` (generator: `--at=/`) makes it the portal root in place of `root to: "dashboard#index"`; delete the generated `DashboardController` and view afterwards. Generate one with `rails g pu:dashboard Overview --dest=<portal>`. See [[plutonium-dashboard]].
533
+
478
534
  ## Custom member / collection routes
479
535
 
480
536
  ```ruby
@@ -491,9 +547,9 @@ register_resource ::Post do
491
547
  end
492
548
  ```
493
549
 
494
- **Always pass `as:`.** Without it, `resource_url_for(@post, action: :preview)` fails because there's no named route to look up — especially critical for nested resources.
550
+ **Always pass `as:`.** Without it, `resource_url_for(@post, action: :preview)` fails because there's no named route to look up, which is especially critical for nested resources.
495
551
 
496
- For most operations with business logic, prefer **interactive actions** (definition + interaction — see [[plutonium-resource]] › Actions) over custom controller routes. The action routes are wired automatically with no `register_resource` block needed.
552
+ For most operations with business logic, prefer **interactive actions** (definition + interaction, see [[plutonium-resource]] › Actions) over custom controller routes. The action routes are wired automatically with no `register_resource` block needed.
497
553
 
498
554
  ## Cross-package and nested URLs
499
555
 
@@ -501,7 +557,7 @@ See [[plutonium-behavior]] for full `resource_url_for` signature and [[plutonium
501
557
 
502
558
  ---
503
559
 
504
- # Part 5 — Connecting Resources to Portals (`pu:res:conn`)
560
+ # Part 5: Connecting Resources to Portals (`pu:res:conn`)
505
561
 
506
562
  A resource is invisible until connected to at least one portal. The generator wires up the portal-specific controller, policy, definition, and route registration.
507
563
 
@@ -511,7 +567,7 @@ A resource is invisible until connected to at least one portal. The generator wi
511
567
  rails g pu:res:conn RESOURCE [RESOURCE...] --dest=PORTAL_NAME [--singular]
512
568
  ```
513
569
 
514
- Pass resources directly — avoids interactive prompts. No `--src` needed.
570
+ Pass resources directly; this avoids interactive prompts. No `--src` needed.
515
571
 
516
572
  ## Usage
517
573
 
@@ -526,7 +582,7 @@ rails g pu:res:conn Blogging::Post Blogging::Comment --dest=admin_portal
526
582
  rails g pu:res:conn Profile --dest=customer_portal --singular
527
583
  ```
528
584
 
529
- **Run after migrations** — the generator reads model columns to seed the policy's `permitted_attributes_for_*`.
585
+ **Run after migrations**: the generator reads model columns to seed the policy's `permitted_attributes_for_*`.
530
586
 
531
587
  ## What gets generated
532
588
 
@@ -546,7 +602,7 @@ register_resource ::Post
546
602
  register_resource ::Profile, singular: true # if --singular
547
603
  ```
548
604
 
549
- Re-running `pu:res:conn` for the same resource is **idempotent** — already-registered entries report `identical` and are not duplicated. Insertion falls back gracefully when the conventional `# register resources above` marker is missing (uses the `routes.draw do` opening), and warns clearly if it can't find any anchor.
605
+ Re-running `pu:res:conn` for the same resource is **idempotent**: already-registered entries report `identical` and are not duplicated. Insertion falls back gracefully when the conventional `# register resources above` marker is missing (uses the `routes.draw do` opening), and warns clearly if it can't find any anchor.
550
606
 
551
607
  ### Generated controller
552
608
 
@@ -576,7 +632,7 @@ class AdminPortal::PostPolicy < ::PostPolicy
576
632
  end
577
633
  ```
578
634
 
579
- Review and trim — the generator is liberal. Especially: drop `_id` fields when the form uses the association name, and add `:price` (not `:price_cents`) for `has_cents` fields.
635
+ Review and trim: the generator is liberal. Especially: drop `_id` fields when the form uses the association name, and add `:price` (not `:price_cents`) for `has_cents` fields.
580
636
 
581
637
  ---
582
638
 
@@ -607,9 +663,9 @@ Review and trim — the generator is liberal. Especially: drop `_id` fields when
607
663
 
608
664
  ## Related skills
609
665
 
610
- - [[plutonium-resource]] — what a resource IS (model + definition + scaffold options)
611
- - [[plutonium-behavior]] — controllers, policies, interactions
612
- - [[plutonium-tenancy]] — entity scoping, nested resources, invites
613
- - [[plutonium-auth]] — Rodauth account configuration
614
- - [[plutonium-ui]] — layouts, page classes, custom Phlex components, assets
615
- - [[plutonium-testing]] — testing portals, packages, controllers
666
+ - [[plutonium-resource]]: what a resource IS (model + definition + scaffold options)
667
+ - [[plutonium-behavior]]: controllers, policies, interactions
668
+ - [[plutonium-tenancy]]: entity scoping, nested resources, invites
669
+ - [[plutonium-auth]]: Rodauth account configuration
670
+ - [[plutonium-ui]]: layouts, page classes, custom Phlex components, assets
671
+ - [[plutonium-testing]]: testing portals, packages, controllers