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
@@ -0,0 +1,123 @@
1
+ # Dashboard DSL Reference
2
+
3
+ ::: warning Experimental
4
+ Dashboards are experimental: the DSL and behavior may change in a future release.
5
+ :::
6
+
7
+ A dashboard is a subclass of `Plutonium::Dashboard::Base`. Everything is declared at class level; an instance is built per request with the view context and is the receiver of every card block.
8
+
9
+ ## Presentation
10
+
11
+ ```ruby
12
+ presents label: "Sales", description: "Orders and revenue", icon: Phlex::TablerIcons::ChartBar
13
+ ```
14
+
15
+ | Key | Default |
16
+ |---|---|
17
+ | `label` | `plutonium.dashboards.<key>.label`, then the class name without `Dashboard` |
18
+ | `description` | `plutonium.dashboards.<key>.description`, then nothing |
19
+ | `icon` | `Phlex::TablerIcons::LayoutDashboard` (used in the sidebar) |
20
+
21
+ `<key>` is `Class.i18n_key`: the class name underscored without the suffix (`AdminPortal::SalesDashboard` → `admin_portal/sales`). `label` and `description` accept the class-level lazy `t("...")`.
22
+
23
+ ## Board-level options
24
+
25
+ ### refresh(seconds)
26
+
27
+ Default reload interval for every lazy card. `nil` (the default) disables it.
28
+
29
+ ### width(token)
30
+
31
+ Page width, one of `:sm`, `:md`, `:lg`, `:xl`, `:full`. Default `:full`.
32
+
33
+ ## Cards
34
+
35
+ ```ruby
36
+ metric(key, **options) { ... }
37
+ chart(key, **options) { ... }
38
+ card(key, **options) { ... }
39
+ ```
40
+
41
+ Keys are unique per dashboard; a duplicate raises at class load. Cards render in declaration order.
42
+
43
+ ### Common options
44
+
45
+ | Option | Type | Default | Meaning |
46
+ |---|---|---|---|
47
+ | `label:` | String, lazy `t` | convention, then key titleized | Title |
48
+ | `description:` | String, lazy `t` | convention | Caption |
49
+ | `icon:` | Phlex icon class | none | Shown beside the title |
50
+ | `span:` | `1`..`12`, `:full` | metric `3`, chart `6`, card `6` | Columns of the 12-column grid. `:full` is `12`. On tablets (2 columns) a span of `6` or more takes the row; phones are one column |
51
+ | `lazy:` | Boolean | `true` | `true`: own lazy turbo frame, block runs in a separate request. `false`: inline, block runs in the page request (adding its query time to every page load); no frame, never refreshes. When `config.consider_all_requests_local` is true (development and test), an inline card's exception fails the whole page. See the [guide](/guides/dashboards#lazy-and-inline-cards) |
52
+ | `refresh:` | Integer seconds, or `false` | dashboard `refresh` | Reload interval; requires `lazy: true` (a number on a `lazy: false` card raises `ArgumentError`). `false` opts the card out of the dashboard's `refresh` |
53
+ | `condition:` | Proc, Symbol | none | Hides the card and 404s its endpoint when false |
54
+ | `href:` | String, Proc | none | Links the title |
55
+
56
+ Procs (`condition:`, `href:`, the block) run on the dashboard instance with `instance_exec`; a one-argument proc receives the instance instead.
57
+
58
+ ### Locale conventions
59
+
60
+ ```
61
+ plutonium.dashboards.<key>.cards.<card>.label
62
+ plutonium.dashboards.<key>.cards.<card>.description
63
+ plutonium.portals.<portal>.dashboards.<key>...
64
+ ```
65
+
66
+ ### metric
67
+
68
+ The block returns a value or a hash.
69
+
70
+ | Hash key | Meaning |
71
+ |---|---|
72
+ | `value` | The number or string. `nil` renders `plutonium.dashboard.metric.empty` |
73
+ | `previous` | Computes `change` as a percentage from this value; a zero previous yields no percentage |
74
+ | `change` | Numeric percentage points (`2.5` → `+2.5%`) or a string shown as-is |
75
+ | `trend` | `:up`, `:down`, `:flat`; inferred from the sign of `change` |
76
+ | `change_label` | Caption after the change |
77
+
78
+ | Option | Values | Meaning |
79
+ |---|---|---|
80
+ | `format:` | `:number` (default), `:currency`, `:percentage`, `:human`, Proc | How `value` is rendered. A proc receives the value and runs on the dashboard |
81
+ | `precision:` | Integer | Decimal places; defaults per format (2 for currency, 1 for percentage, 3 significant for human) |
82
+ | `unit:` | String | Currency symbol for `:currency` |
83
+ | `prefix:` / `suffix:` | String | Wrapped around the formatted value |
84
+ | `positive:` | `:up` (default), `:down` | Which direction is good; drives the change colour |
85
+ | `change_label:` | String | Same as the hash key |
86
+
87
+ ### chart
88
+
89
+ The block returns Chartkick data: `{label => value}`, `[[label, value], ...]` or `[{name:, data:}, ...]`. `Date` and `Time` keys serialise as ISO strings and draw a time axis.
90
+
91
+ | Option | Values | Meaning |
92
+ |---|---|---|
93
+ | `type:` | `:line` (default), `:area`, `:column`, `:bar`, `:pie`, `:donut`, `:scatter` | Chartkick chart class |
94
+ | `height:` | CSS length | Drawing area height, default `"240px"` |
95
+ | anything else | | Passed to Chartkick unchanged: `colors`, `stacked`, `min`, `max`, `prefix`, `suffix`, `thousands`, `decimal`, `xtitle`, `ytitle`, `legend`, `curve`, `points`, `discrete`, `download`, `library`, ... |
96
+
97
+ The `chart` Stimulus controller merges these over Plutonium's defaults: series colours from `--pu-chart-1` to `--pu-chart-8`, axis text from `--pu-text-muted`, grid lines from `--pu-border`, and the empty-data message from `plutonium.js.charts.empty`. A colour-mode switch redraws every chart.
98
+
99
+ ### card
100
+
101
+ No kind-specific options. The block is `instance_exec`ed in `Plutonium::UI::Dashboard::Custom`, a Phlex component: it emits markup directly, and any method the component does not define is forwarded to the dashboard instance.
102
+
103
+ ## Instance API
104
+
105
+ | Method | Meaning |
106
+ |---|---|
107
+ | `authorize?` | Override to gate the page and every card. Default `true` |
108
+ | `visible_cards` | Cards whose `condition:` passes |
109
+ | `visible_card!(key)` | The card, or `Plutonium::Dashboard::UnknownCardError` (404) |
110
+ | `refresh_for(card)` | The card's interval, else the dashboard's; `nil` for a card declared `refresh: false` |
111
+ | `view_context` / `helpers` | The Rails view context |
112
+ | `current_user`, `current_scoped_entity`, `scoped_to_entity?`, `params`, `request`, `controller`, `current_engine`, `resource_url_for`, `authorized_resource_scope`, `allowed_to?`, `policy_for`, `registered_resources`, `root_path` | Delegated to the view context |
113
+
114
+ ## Class API
115
+
116
+ | Method | Meaning |
117
+ |---|---|
118
+ | `cards` | All declared cards, in order |
119
+ | `find_card(key)` / `find_card!(key)` | Lookup by key |
120
+ | `label`, `description`, `icon` | Resolved presentation |
121
+ | `i18n_key`, `route_name` | `admin_portal/sales`, `sales` |
122
+
123
+ Subclasses inherit the parent's cards and options by copy, so a portal-specific subclass can add cards without touching the parent.
@@ -0,0 +1,35 @@
1
+ # Dashboard Reference
2
+
3
+ ::: warning Experimental
4
+ Dashboards are experimental: the DSL and behavior may change in a future release.
5
+ :::
6
+
7
+ Reference documentation for Plutonium dashboards: pages of metric, chart and free-form cards, each loaded in its own lazy turbo frame by default.
8
+
9
+ ## In this section
10
+
11
+ | Page | What it covers |
12
+ |------|---------------|
13
+ | [DSL](/reference/dashboard/dsl) | `Plutonium::Dashboard::Base`, `presents`, `refresh`, `width`, and the `metric` / `chart` / `card` macros with every option |
14
+ | [Registration](/reference/dashboard/registration) | `register_dashboard`, the routes it draws, the synthesized controller, the sidebar, and the `pu:dashboard` generator |
15
+
16
+ ## Quick start
17
+
18
+ ```ruby
19
+ # packages/admin_portal/app/dashboards/admin_portal/sales_dashboard.rb
20
+ module AdminPortal
21
+ class SalesDashboard < Plutonium::Dashboard::Base
22
+ presents label: "Sales", icon: Phlex::TablerIcons::ChartBar
23
+
24
+ metric(:orders) { {value: Order.this_month.count, previous: Order.last_month.count} }
25
+ chart(:revenue, type: :area, span: 8) { Order.group_by_day(:created_at).sum(:total) }
26
+ end
27
+ end
28
+
29
+ # packages/admin_portal/config/routes.rb
30
+ AdminPortal::Engine.routes.draw do
31
+ register_dashboard AdminPortal::SalesDashboard, at: "sales"
32
+ end
33
+ ```
34
+
35
+ See the [Dashboards guide](/guides/dashboards) for a full walkthrough.
@@ -0,0 +1,76 @@
1
+ # Dashboard Registration
2
+
3
+ ::: warning Experimental
4
+ Dashboards are experimental: the DSL and behavior may change in a future release.
5
+ :::
6
+
7
+ ## register_dashboard
8
+
9
+ ```ruby
10
+ AdminPortal::Engine.routes.draw do
11
+ register_dashboard AdminPortal::HomeDashboard, at: "/" # the portal root
12
+ register_dashboard AdminPortal::SalesDashboard, at: "sales" # /admin/dashboards/sales
13
+ register_dashboard Reports::WeeklyDashboard, at: "reports/weekly", as: "weekly"
14
+ end
15
+ ```
16
+
17
+ | Argument | Meaning |
18
+ |---|---|
19
+ | `at:` | Path under the portal's `dashboards/` segment. `"/"` or `""` mounts at the engine root |
20
+ | `prefix:` | Leading path segment, `"dashboards"` by default. `nil` draws the mount at the bare `at:` path (`/admin/sales`); a string swaps the segment (`prefix: "reports"` → `/admin/reports/sales`). Does not affect helper names |
21
+ | `as:` | Route helper prefix. Defaults to `at:` with slashes replaced, or the class slug for a root mount |
22
+
23
+ ### Routes drawn
24
+
25
+ ```
26
+ GET /dashboards/sales → DashboardsController#show sales_dashboard_path
27
+ GET /dashboards/sales/cards/:card → DashboardsController#card sales_dashboard_card_path
28
+ ```
29
+
30
+ Every path is drawn under `dashboards/`, so a dashboard never shadows a `register_resource` route of the same name (`at: "sales"` next to a `Sale` resource). The route helpers carry no prefix.
31
+
32
+ With `prefix: nil` nothing reserves the path, so keep `at:` clear of your resource routes yourself.
33
+
34
+ A root mount draws `root` for the page and `/dashboards/<as>/cards/:card` for the cards (`/<as>/cards/:card` with `prefix: nil`). Replace the portal's generated `root to: "dashboard#index"` line with the registration; two root routes clash. The generated `DashboardController` and its view are then unrouted and can be deleted. The [guide](/guides/dashboards#replacing-the-portal-s-default-page) walks through it.
35
+
36
+ On a `:path` entity-scoped portal the routes sit inside the scope segment and the helpers are prefixed (`organization_scoped_sales_dashboard_path`). URLs built by the framework thread the current tenant through; when building your own use `dashboard_path_for(SalesDashboard)`, available in controllers, views and components.
37
+
38
+ ### The controller
39
+
40
+ `register_dashboard` synthesizes `<Portal>::DashboardsController < <Portal>::PlutoniumController` including `Plutonium::Dashboard::Controller` and the portal's `Concerns::Controller`, so it carries the portal's authentication, entity scoping and layout. On the main app it synthesizes a bare `::DashboardsController < ApplicationController`.
41
+
42
+ Define the class yourself to take over; the synthesized one is only created when the constant is missing.
43
+
44
+ ```ruby
45
+ module AdminPortal
46
+ class DashboardsController < PlutoniumController
47
+ include Plutonium::Dashboard::Controller
48
+
49
+ before_action { set_page_title("Reports") }
50
+ end
51
+ end
52
+ ```
53
+
54
+ The concern provides `show` and `card`, `current_dashboard`, `current_dashboard_class`, `authorize_dashboard!` and `dashboard_card_path(card)`.
55
+
56
+ ### The card endpoint
57
+
58
+ `GET <mount>/cards/:card` runs `authorize?`, looks the key up among the dashboard's cards, checks its `condition:`, and renders the card inside `<turbo-frame id="pu-dashboard-card-<key>">`. Unknown or hidden cards respond 404 (`Plutonium::Dashboard::UnknownCardError`); a denied `authorize?` responds 403. A request without a `Turbo-Frame` header renders the card inside the full layout.
59
+
60
+ ## Sidebar
61
+
62
+ `registered_dashboards` (a controller helper, also available in components) lists the dashboards mounted on the current engine, and `dashboard_path_for(klass)` builds each page path. The gem's `_resource_sidebar.html.erb` groups them under a Dashboards item (`plutonium.resource.nav.dashboards`) after the Home link (`plutonium.resource.nav.home`), skipping the one mounted at the root, which the Home link already opens.
63
+
64
+ ## Generator
65
+
66
+ ```bash
67
+ rails g pu:dashboard Sales --dest=admin_portal # /admin/dashboards/sales
68
+ rails g pu:dashboard Home --dest=admin_portal --at=/ # portal root
69
+ rails g pu:dashboard Reporting --dest=main_app --at=reports
70
+ ```
71
+
72
+ Writes `app/dashboards/<portal>/<name>_dashboard.rb` in the package (or `app/dashboards/` for `main_app`) and adds the `register_dashboard` line to the routes. Idempotent.
73
+
74
+ ## Assets
75
+
76
+ Chart cards load `plutonium-charts.js` (Chart.js and Chartkick) on demand; the file ships with the gem beside `plutonium.js` and is precompiled with it. `Plutonium.configuration.assets.charts_script` names the asset (`"plutonium-charts.min.js"` by default). When `PLUTONIUM_DEV` is set the file comes from the `src/build` manifest like the main bundle.
@@ -16,10 +16,10 @@ rails g pu:lite:tune
16
16
 
17
17
  It writes a `pragmas:` mapping:
18
18
 
19
- - `cache_size: -64000` — 64 MB page cache (the ~2 MB default is too small).
20
- - `temp_store: 2` — MEMORY; sorts and temp indexes stay off disk.
21
- - `mmap_size: 536870912` — 512 MB memory-mapped I/O.
22
- - `wal_autocheckpoint: 10000` — checkpoint roughly every 40 MB of WAL.
19
+ - `cache_size: -64000`: 64 MB page cache (the ~2 MB default is too small).
20
+ - `temp_store: 2`: MEMORY; sorts and temp indexes stay off disk.
21
+ - `mmap_size: 536870912`: 512 MB memory-mapped I/O.
22
+ - `wal_autocheckpoint: 10000`: checkpoint roughly every 40 MB of WAL.
23
23
 
24
24
  On Rails &lt; 8.1 it also writes the baseline pragmas (`journal_mode: WAL`,
25
25
  `synchronous: NORMAL`, `foreign_keys: true`, `journal_size_limit`) that Rails 8.1+
@@ -30,7 +30,7 @@ constant-poll busy handler (`busy_handler_timeout`), which has better tail-laten
30
30
  than SQLite's internal exponential backoff. Setting a busy-timeout pragma would
31
31
  replace the better handler with the worse one, so this generator never emits it.
32
32
 
33
- The generator is idempotent — re-running it detects the existing pragmas and skips.
33
+ The generator is idempotent: re-running it detects the existing pragmas and skips.
34
34
  It only ever touches the `default:` block, so a `pragmas:` mapping nested under
35
35
  another environment is left untouched.
36
36
 
@@ -47,7 +47,7 @@ rails g pu:lite:maintenance --schedule="every day at 4am"
47
47
 
48
48
  The job runs `PRAGMA optimize` on every configured SQLite database and `VACUUM`
49
49
  only on databases without live 24/7 writers (`primary`, `errors`, `rails_pulse`
50
- by default — edit `VACUUM_DBS` in the generated job to suit your app).
50
+ by default; edit `VACUUM_DBS` in the generated job to suit your app).
51
51
 
52
52
  **Why VACUUM only some databases?** SolidQueue, Solid Cache and Solid Cable write
53
53
  to their databases constantly. `VACUUM` takes a global *exclusive* lock for its
@@ -61,5 +61,5 @@ Databases listed in the job that don't exist in `config/database.yml` are skippe
61
61
  at runtime, so the same job is safe regardless of which `pu:lite:*` generators you
62
62
  have run.
63
63
 
64
- If `solid_queue` is not installed, the job file is still created but not scheduled —
64
+ If `solid_queue` is not installed, the job file is still created but not scheduled:
65
65
  add a `sqlite_maintenance` entry to whatever scheduler you use.
@@ -0,0 +1,262 @@
1
+ # Internationalization
2
+
3
+ Every string Plutonium renders comes from a locale file, and every label it derives from a key (an action, a scope, a filter, a kanban column, a wizard step, a field's placeholder) can be supplied by one. Nothing in a definition has to change for an app to translate its UI or reword the defaults.
4
+
5
+ ## Where the strings live
6
+
7
+ Plutonium ships its own text under `config/locales/en/*.yml` in the gem, all under the `plutonium` namespace. Rails loads it at the lowest precedence:
8
+
9
+ 1. The host application's `config/locales` wins over everything.
10
+ 2. Package and portal engines' `config/locales`, which Rails loads automatically.
11
+ 3. The gem's defaults.
12
+
13
+ So a portal can reword a Plutonium string for itself, and the app can override the portal.
14
+
15
+ To change a fixed string, copy its key into your own locale file. The gem's locale files are the catalogue:
16
+
17
+ ```yaml
18
+ # config/locales/en.yml
19
+ en:
20
+ plutonium:
21
+ boolean:
22
+ "true": "On"
23
+ "false": "Off"
24
+ ```
25
+
26
+ Pagination is the one surface with its own dictionary. The info sentence, the per-page sentence and the nav aria labels come from [Pagy's](https://ddnexus.github.io/pagy/resources/i18n) locale files, which cover about thirty-five languages. Override a Pagy key by adding a file to `Pagy::I18n.pathnames`, not through Rails I18n.
27
+
28
+ ## Switching locale
29
+
30
+ Plutonium reads `I18n.locale` on every render and never sets it. Set the locale the way any Rails app does, typically an `around_action` in the portal's controller concern:
31
+
32
+ ```ruby
33
+ module AdminPortal
34
+ module Concerns
35
+ module Controller
36
+ extend ActiveSupport::Concern
37
+
38
+ included do
39
+ around_action :switch_locale
40
+ end
41
+
42
+ def switch_locale(&)
43
+ I18n.with_locale(current_user&.locale || I18n.default_locale, &)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ ```
49
+
50
+ The Pagy locale is synced to `I18n.locale` on each pagination render, so the same switch covers pagination.
51
+
52
+ ## Models and attributes
53
+
54
+ Model and attribute names follow the Rails convention and already drive every label, column header, page title and flash:
55
+
56
+ ```yaml
57
+ en:
58
+ activerecord:
59
+ models:
60
+ blogging/post:
61
+ one: "Post"
62
+ other: "Posts"
63
+ attributes:
64
+ blogging/post:
65
+ title: "Title"
66
+ published_at: "Published"
67
+ ```
68
+
69
+ The model key is `model_name.i18n_key`: `blogging/post` for `Blogging::Post`. When a model defines `one` and `other`, Plutonium uses them for the plural forms in page titles, empty states and pagination. Without them it falls back to English inflection.
70
+
71
+ ## Fields: placeholder, hint, description
72
+
73
+ A definition never has to declare help text to make it translatable. When an input or display leaves a slot blank, Plutonium looks it up by convention:
74
+
75
+ ```yaml
76
+ en:
77
+ plutonium:
78
+ fields:
79
+ blogging/post:
80
+ title:
81
+ placeholder: "A short, descriptive title"
82
+ hint: "Shown in search results"
83
+ body:
84
+ description: "Rendered as Markdown"
85
+ ```
86
+
87
+ `placeholder` and `hint` apply to forms. `description` applies to displays. The lookup walks the model's ancestors like `human_attribute_name` does, so a key on `blogging/post` also serves an STI subclass.
88
+
89
+ For each slot the resolution order is:
90
+
91
+ 1. An explicit option on `input` or `display`, then on `field`.
92
+ 2. `plutonium.portals.<portal>.fields.<model>.<attr>.<slot>` when rendering inside a portal.
93
+ 3. `plutonium.fields.<model>.<attr>.<slot>`.
94
+ 4. `helpers.placeholder.<model>.<attr>`, the Rails `form_with` convention, for placeholders only.
95
+ 5. Nothing.
96
+
97
+ An explicit option can still be a translation. Use the definition's class-level `t`, which returns a lazy value resolved on every render in the request's locale:
98
+
99
+ ```ruby
100
+ class PostDefinition < Plutonium::Resource::Definition
101
+ input :email, placeholder: t("forms.shared.email_placeholder")
102
+ input :quantity, hint: t("forms.stock.hint", count: 3)
103
+ end
104
+ ```
105
+
106
+ `t` is sugar over the proc form, which also works and has always been locale-safe:
107
+
108
+ ```ruby
109
+ input :email, placeholder: -> { I18n.t("forms.shared.email_placeholder") }
110
+ ```
111
+
112
+ Never call `I18n.t` directly in a class body. It runs once, at load time, in whatever locale happened to be active. Literal strings remain allowed and are used as they are.
113
+
114
+ The page-title setters take the same values. Pass a literal to fix it, or the lazy `t` to translate it per request:
115
+
116
+ ```ruby
117
+ index_page_title t("blog.index.title")
118
+ index_page_description t("blog.index.description")
119
+ ```
120
+
121
+ When a definition sets no title, the page falls back to the resource's translated model name.
122
+
123
+ The setter's proc is called with no record context, so a title that depends on the record belongs in a `page_title` override on the nested page class (`class ShowPage < ShowPage`), where `object` is available.
124
+
125
+ ## Your own components and pages
126
+
127
+ Text you add to the UI goes in a locale file too. Which `t` you call depends on where the code runs:
128
+
129
+ | Where | Call |
130
+ |---|---|
131
+ | Components, pages, and nested `Form` / `Display` / `Table` classes (render hooks included) | `t("full.key", **opts)`, a protected method from `Plutonium::UI::Component::Behaviour` |
132
+ | `display` / `input` / `column` blocks in a definition | `t("full.key")`, since the block runs inside the rendering component |
133
+ | Definition class body (`label:`, `hint:`, `placeholder:`, page titles) | the class-level lazy `t("full.key")` above |
134
+ | A bare Phlexi field component that does not include `Behaviour` | `Plutonium::Translation.t("full.key")` |
135
+
136
+ All of them take full keys only. Use one key per sentence with `%{name}` placeholders rather than concatenating fragments around a value, and `count:` for plurals:
137
+
138
+ ```ruby
139
+ def render_before_content
140
+ div(class: "pu-alert pu-alert-info", role: "status") do
141
+ div(class: "pu-alert-message") { t("blog.posts.show.comment_count", count: object.comments.count) }
142
+ end
143
+ end
144
+ ```
145
+
146
+ ## Derived labels
147
+
148
+ Actions, scopes, filters, kanban columns and wizard steps default to a humanized version of their key. Each has a convention key that takes precedence over that fallback, and a `label:` option that takes precedence over both. For an action backed by an interaction, the interaction's explicit `presents label:` also counts as declared and wins over the convention; only the class-name default yields to it.
149
+
150
+ ```yaml
151
+ en:
152
+ plutonium:
153
+ actions:
154
+ blogging/post:
155
+ publish: "Publish now"
156
+ scopes:
157
+ blogging/post:
158
+ drafts: "Drafts"
159
+ filters:
160
+ blogging/post:
161
+ author: "Written by"
162
+ kanban_columns:
163
+ blogging/post:
164
+ in_review: "In review"
165
+ wizard_steps:
166
+ onboarding_wizard:
167
+ billing: "Billing details"
168
+ ```
169
+
170
+ The model segment is the resource's `model_name.i18n_key`. For wizard steps it is the wizard's `model_name.i18n_key`.
171
+
172
+ Enum values shown as badges, and the value pills of a select filter, resolve the way Rails resolves enum attribute values, with a Plutonium key as an alternative:
173
+
174
+ ```yaml
175
+ en:
176
+ activerecord:
177
+ attributes:
178
+ blogging/post:
179
+ status/draft: "Draft"
180
+ status/published: "Live"
181
+ plutonium:
182
+ values:
183
+ blogging/post:
184
+ status:
185
+ archived: "Archived"
186
+ ```
187
+
188
+ ## Portals
189
+
190
+ Every convention key has a portal-scoped variant under `plutonium.portals.<portal>`, where `<portal>` is the package namespace (`admin_portal`). It wins over the global key while rendering inside that portal, and the main app has no portal segment.
191
+
192
+ Portal packages are Rails engines, so their `config/locales` directory loads automatically. The portal generator scaffolds `packages/<portal>/config/locales/en.yml` with the prefix in place:
193
+
194
+ ```yaml
195
+ # packages/admin_portal/config/locales/en.yml
196
+ en:
197
+ plutonium:
198
+ portals:
199
+ admin_portal:
200
+ fields:
201
+ blogging/post:
202
+ title:
203
+ placeholder: "Internal working title"
204
+ actions:
205
+ blogging/post:
206
+ publish: "Publish to site"
207
+ ```
208
+
209
+ Feature packages get the same file for their models' names, attributes and field text. Keys without the portal prefix apply everywhere, and load order settles conflicts: the app's files beat a package's, and a package's beat the gem's.
210
+
211
+ Locale keys vary by portal, never by tenant. Tenant-specific wording is a custom I18n backend concern and is out of scope.
212
+
213
+ ## JavaScript
214
+
215
+ The Stimulus controllers bundled with the gem read their strings from a JSON blob the layout renders once per page, in the current locale:
216
+
217
+ ```html
218
+ <meta name="pu-i18n" content="{&quot;clipboard&quot;:{&quot;copied&quot;:&quot;Copied!&quot;},...}">
219
+ ```
220
+
221
+ The blob is the `plutonium.js` subtree of the locale merged over the default locale, so overriding a key in YAML changes the bundled JavaScript's text without rebuilding it, and a partially translated locale falls back per key. Host code can read the same blob:
222
+
223
+ ```js
224
+ window.Plutonium.t("plutonium.js.turbo_confirm.confirm")
225
+ window.Plutonium.t("plutonium.js.attachment_input.delete_all", { count: 3 })
226
+ ```
227
+
228
+ Third-party widgets keep their own locale mechanisms. The blob carries optional pass-through settings under `plutonium.js.libraries`:
229
+
230
+ | Key | Passed to |
231
+ |---|---|
232
+ | `slim_select` | Slim Select `settings` (`placeholderText`, `searchText`, `searchPlaceholder`, `searchingText`) |
233
+ | `flatpickr.locale` | flatpickr's `locale` option. Loading the matching `l10n` bundle is the host's job. |
234
+ | `uppy.strings` | Uppy `locale.strings` |
235
+ | `intl_tel_input` | intl-tel-input `i18n` |
236
+
237
+ ## Auth
238
+
239
+ Rodauth's own text (labels, buttons, flashes, email subjects) is translated by the [rodauth-i18n](https://github.com/janko/rodauth-i18n) gem, which Plutonium's Rodauth views pick up unchanged because they render through Rodauth's configuration methods. The mailer templates the Rodauth generator copies into your app are plain ERB and are yours to translate.
240
+
241
+ ## Testing
242
+
243
+ Turn on missing-translation errors in the test environment so a rendered component with a missing key fails instead of printing its key:
244
+
245
+ ```ruby
246
+ # config/environments/test.rb
247
+ config.i18n.raise_on_missing_translations = true
248
+ ```
249
+
250
+ Convention lookups are exempt. They probe with `default: nil` and render nothing when a key is absent, which is the intended behaviour, so they never raise.
251
+
252
+ ## Adding a language
253
+
254
+ For a second locale, provide:
255
+
256
+ 1. A [rails-i18n](https://github.com/svenfuchs/rails-i18n) locale for Rails' own strings, dates and numbers.
257
+ 2. A Pagy dictionary for pagination, which Pagy ships for most languages.
258
+ 3. `rodauth-i18n` if you use Rodauth.
259
+ 4. A `plutonium` namespace translated from the gem's `config/locales/en/*.yml`.
260
+ 5. Your models, attributes and field text under the keys above.
261
+
262
+ Plutonium itself ships only `en`.
@@ -7,14 +7,16 @@ aside: false
7
7
  <SectionLanding
8
8
  eyebrow="Reference"
9
9
  title="Every API, in one place."
10
- lede="The full surface area of Plutonium — controllers, policies, definitions, fields, interactions, generators."
10
+ lede="The full surface area of Plutonium: controllers, policies, definitions, fields, interactions, generators."
11
11
  mode="categorized"
12
12
  :rail="[
13
13
  { group: 'App', items: [
14
14
  { name: 'Overview', link: '/plutonium-core/reference/app/' },
15
+ { name: 'Configuration', link: '/plutonium-core/reference/configuration' },
15
16
  { name: 'Packages', link: '/plutonium-core/reference/app/packages' },
16
17
  { name: 'Portals', link: '/plutonium-core/reference/app/portals' },
17
18
  { name: 'Generators', link: '/plutonium-core/reference/app/generators' },
19
+ { name: 'Lite (SQLite) generators', link: '/plutonium-core/reference/generators/lite' },
18
20
  ]},
19
21
  { group: 'Resource', items: [
20
22
  { name: 'Overview', link: '/plutonium-core/reference/resource/' },
@@ -22,13 +24,15 @@ aside: false
22
24
  { name: 'Definition', link: '/plutonium-core/reference/resource/definition' },
23
25
  { name: 'Query', link: '/plutonium-core/reference/resource/query' },
24
26
  { name: 'Actions', link: '/plutonium-core/reference/resource/actions' },
25
- { name: 'Positioning', link: '/plutonium-core/reference/positioning' },
27
+ { name: 'Positioning', link: '/plutonium-core/reference/resource/positioning' },
28
+ { name: 'CSV Export', link: '/plutonium-core/reference/resource/export' },
26
29
  ]},
27
30
  { group: 'Behavior', items: [
28
31
  { name: 'Overview', link: '/plutonium-core/reference/behavior/' },
29
32
  { name: 'Controllers', link: '/plutonium-core/reference/behavior/controllers' },
30
33
  { name: 'Policies', link: '/plutonium-core/reference/behavior/policies' },
31
34
  { name: 'Interactions', link: '/plutonium-core/reference/behavior/interactions' },
35
+ { name: 'Async interactions', link: '/plutonium-core/reference/behavior/async-interactions' },
32
36
  ]},
33
37
  { group: 'UI', items: [
34
38
  { name: 'Overview', link: '/plutonium-core/reference/ui/' },
@@ -65,6 +69,11 @@ aside: false
65
69
  { name: 'Positioning', link: '/plutonium-core/reference/kanban/positioning' },
66
70
  { name: 'Authorization', link: '/plutonium-core/reference/kanban/authorization' },
67
71
  ]},
72
+ { group: 'Dashboard', items: [
73
+ { name: 'Overview', link: '/plutonium-core/reference/dashboard/' },
74
+ { name: 'DSL', link: '/plutonium-core/reference/dashboard/dsl' },
75
+ { name: 'Registration', link: '/plutonium-core/reference/dashboard/registration' },
76
+ ]},
68
77
  { group: 'Testing', items: [
69
78
  { name: 'Overview', link: '/plutonium-core/reference/testing/' },
70
79
  ]},
@@ -5,7 +5,7 @@
5
5
  Every drag-and-drop move is authorized through the `kanban_move?` method on the resource's policy. The default implementation delegates to `update?`:
6
6
 
7
7
  ```ruby
8
- # Plutonium::Resource::Policy — built-in default
8
+ # Plutonium::Resource::Policy (built-in default)
9
9
  def kanban_move?
10
10
  update?
11
11
  end
@@ -29,7 +29,7 @@ end
29
29
 
30
30
  ## Gating a specific transition (`from` / `to` context)
31
31
 
32
- `kanban_move?` is the **single** authorization for every move — plain moves and `enter_interaction:` columns alike. To gate a *specific* transition, read the source and destination columns from the authorization context. They are exposed as the optional `kanban_from` / `kanban_to` policy readers (the `Plutonium::Kanban::Column` objects), and are `nil` for every non-move authorization:
32
+ `kanban_move?` is the **single** authorization for every move (plain moves and `enter_interaction:` columns alike). To gate a *specific* transition, read the source and destination columns from the authorization context. They are exposed as the optional `kanban_from` / `kanban_to` policy readers (the `Plutonium::Kanban::Column` objects), and are `nil` for every non-move authorization:
33
33
 
34
34
  ```ruby
35
35
  class DealPolicy < ResourcePolicy
@@ -42,28 +42,28 @@ class DealPolicy < ResourcePolicy
42
42
  end
43
43
  ```
44
44
 
45
- Rules take no positional arguments in ActionPolicy — the columns arrive via context, which the controller supplies on the `kanban_move?` check (`context: { kanban_from:, kanban_to: }`). This replaces per-column policy methods: an `enter_interaction:` column is authorized by `kanban_move?` too, so there is no separate `mark_lost?`-style predicate to define.
45
+ Rules take no positional arguments in ActionPolicy; the columns arrive via context, which the controller supplies on the `kanban_move?` check (`context: { kanban_from:, kanban_to: }`). This replaces per-column policy methods: an `enter_interaction:` column is authorized by `kanban_move?` too, so there is no separate `mark_lost?`-style predicate to define.
46
46
 
47
47
  Both `kanban_from` and `kanban_to` are **trustworthy** to authorize on. `to` is where the card ends up; and although `from_column` arrives from the client, the move handler **verifies the record actually resides in the claimed source column** before it proceeds (a mismatch snaps the drag back), so a spoofed or stale `from` can never drive a move past a `kanban_from`-based rule.
48
48
 
49
49
  ## Read-only board
50
50
 
51
- When `kanban_move?` returns `false` for the current user, the board is rendered read-only. Cards are displayed but dragging is disabled — no drag handles appear and the Stimulus controller does not register drop zones.
51
+ When `kanban_move?` returns `false` for the current user, the board is rendered read-only. Cards are displayed but dragging is disabled: no drag handles appear and the Stimulus controller does not register drop zones.
52
52
 
53
53
  ## Authorization flow on a move
54
54
 
55
55
  When a card is dropped, the server:
56
56
 
57
57
  1. Finds the record within the current authorized scope (the same policy `relation_scope` used by the index action).
58
- 2. Calls `authorize_current!(record, to: :kanban_move?, context: { kanban_from:, kanban_to: })` — the single authorization for the move (an `enter_interaction:` column rides on this same check, with no policy method of its own). A `false` result halts the action with HTTP 403.
59
- 3. Verifies the record actually resides in the claimed source column (`from_column` is client-supplied). A mismatch responds 422 and snaps the card back — this is what makes `kanban_from` safe to authorize on.
60
- 4. Validates the drop against the destination column's `accepts:` policy and `locked:` flag. A rejected drop responds with HTTP 422 and re-renders the source column (the Stimulus controller snaps the card back).
58
+ 2. Calls `authorize_current!(record, to: :kanban_move?, context: { kanban_from:, kanban_to: })`: the single authorization for the move (an `enter_interaction:` column rides on this same check, with no policy method of its own). A `false` result halts the action with HTTP 403.
59
+ 3. Verifies the record actually resides in the claimed source column (`from_column` is client-supplied). A mismatch responds 422 and snaps the card back; this is what makes `kanban_from` safe to authorize on.
60
+ 4. Validates the drop against the destination column's `accepts:` policy and the source column's `locked:` flag (a locked column lets no card leave). A rejected drop responds with HTTP 422 and re-renders the source column (the Stimulus controller snaps the card back).
61
61
  5. Enforces the destination column's `wip:` limit (cross-column moves only). Exceeding the WIP cap also responds 422.
62
- 6. Calls `on_enter` and repositions the record inside a transaction.
62
+ 6. Calls the source column's `on_exit`, then the destination's `on_enter`, and repositions the record, all inside a transaction.
63
63
 
64
64
  ## No permitted attributes for moves
65
65
 
66
- Kanban moves do **not** pass through `permitted_attributes_for_update` / `permitted_attributes_for_kanban_move`. The `on_enter` callback is author code that runs with full model access — it is the responsibility of the `on_enter` implementation to assign only the attributes appropriate for a column transition. This is intentional: the callback is trusted Ruby, not user-supplied form data.
66
+ Kanban moves do **not** pass through `permitted_attributes_for_update` / `permitted_attributes_for_kanban_move`. The `on_enter` callback is author code that runs with full model access; it is the responsibility of the `on_enter` implementation to assign only the attributes appropriate for a column transition. This is intentional: the callback is trusted Ruby, not user-supplied form data.
67
67
 
68
68
  ## Column-level drop policies
69
69