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
@@ -1,7 +1,7 @@
1
1
  # CSV Export
2
2
 
3
3
  Every resource ships with a streamed CSV export, **disabled by default**. It is *not* an
4
- [action](./actions.md) — it streams a file and opens in a new tab — so it is enabled
4
+ [action](./actions.md) (it streams a file and opens in a new tab), so it is enabled
5
5
  through the policy rather than declared with `action :export_csv`. The route
6
6
  (`GET /<resources>/export_csv`) is auto-mounted on every resource; a split "Export"
7
7
  button appears on the index page once the policy permits it.
@@ -25,8 +25,8 @@ The control is a split button with two behaviours:
25
25
 
26
26
  | | Source | Filename |
27
27
  |---|---|---|
28
- | **Export** (primary) | The current view — selected scope + filters + search (the index's `?q`), **all** matching rows (not just the visible page) | `posts_<date>.csv` |
29
- | **Export all** (dropdown) | The entire authorized scope — ignores scope, filters, search, and default scope | `posts_all_<date>.csv` |
28
+ | **Export** (primary) | The current view: selected scope + filters + search (the index's `?q`), **all** matching rows (not just the visible page) | `posts_<date>.csv` |
29
+ | **Export all** (dropdown) | The entire authorized scope: ignores scope, filters, search, and default scope | `posts_all_<date>.csv` |
30
30
 
31
31
  "Export all" always exports everything the user is authorized to read, regardless of the
32
32
  current scope/filters.
@@ -73,12 +73,12 @@ For a column **without** an `export` block, the value is read straight off the r
73
73
  (`record.public_send(name)`):
74
74
 
75
75
  - **Scalars** (strings, numbers, booleans, dates) are written as-is.
76
- - **Associations** render as their display label — the same `display_name_of` the index
77
- uses (e.g. `User #5`, or the record's `to_label`/`name`/`title` if defined) — never
76
+ - **Associations** render as their display label: the same `display_name_of` the index
77
+ uses (e.g. `User #5`, or the record's `to_label`/`name`/`title` if defined), never
78
78
  `#<User:0x…>`. Add an `export` block to export a specific field instead (e.g. the email).
79
79
  - A name that is **neither** an `export` block **nor** a real method on the record renders
80
80
  the placeholder `<<invalid column>>` rather than aborting the (already-streaming) download.
81
- To export a computed or virtual column, give it an `export` block — a `label:`-only
81
+ To export a computed or virtual column, give it an `export` block: a `label:`-only
82
82
  `export` does **not** supply a value, so it too renders the placeholder.
83
83
 
84
84
  ## Notes & limits
@@ -90,5 +90,9 @@ For a column **without** an `export` block, the value is read straight off the r
90
90
  intercepting the streamed download.
91
91
  - **CSV/formula injection** is neutralized: any cell whose value begins with `=`, `+`,
92
92
  `-`, `@`, or a leading tab/CR is prefixed with a single quote so spreadsheet apps import
93
- it as literal text instead of executing it as a formula.
93
+ it as literal text instead of executing it as a formula. The check is run against the
94
+ NFKC-normalized value, so full-width (double-byte) variants (`=`, `+`, `-`, `@`) that
95
+ East Asian locales treat as formula initiators are neutralized too; the original
96
+ (un-normalized) value is what gets the quote prefix, so legitimate full-width content
97
+ is preserved verbatim.
94
98
  - `csv` is a runtime dependency of Plutonium (it is no longer a Ruby default gem on 3.4+).
@@ -1,14 +1,14 @@
1
1
  # Resource Reference
2
2
 
3
- A **resource** is the unit Plutonium gives you full CRUD for — list, show, create, edit, delete — automatically. It's four cooperating layers, plus an optional fifth for business logic.
3
+ A **resource** is the unit Plutonium gives you full CRUD for (list, show, create, edit, delete) automatically. It's four cooperating layers, plus an optional fifth for business logic.
4
4
 
5
5
  | Layer | File | What it controls |
6
6
  |---|---|---|
7
7
  | [Model](./model) | `app/models/post.rb` | Data, validations, associations |
8
- | [Definition](./definition) | `app/definitions/post_definition.rb` | UI — which fields, how they render, what actions exist |
9
- | Policy | `app/policies/post_policy.rb` | Authorization — see [Behavior › Policy](/reference/behavior/policies) |
10
- | Controller | `app/controllers/posts_controller.rb` | Request handling — see [Behavior › Controller](/reference/behavior/controllers) |
11
- | Interaction *(optional)* | `app/interactions/publish_post_interaction.rb` | Business logic for custom actions — see [Behavior › Interaction](/reference/behavior/interactions) |
8
+ | [Definition](./definition) | `app/definitions/post_definition.rb` | UI: which fields, how they render, what actions exist |
9
+ | Policy | `app/policies/post_policy.rb` | Authorization: see [Behavior › Policy](/reference/behavior/policies) |
10
+ | Controller | `app/controllers/posts_controller.rb` | Request handling: see [Behavior › Controller](/reference/behavior/controllers) |
11
+ | Interaction *(optional)* | `app/interactions/publish_post_interaction.rb` | Business logic for custom actions, see [Behavior › Interaction](/reference/behavior/interactions) |
12
12
 
13
13
  ## How a resource is born
14
14
 
@@ -22,7 +22,7 @@ That single scaffold gives you a working model + migration + controller + policy
22
22
 
23
23
  ## Auto-detection is the default
24
24
 
25
- Plutonium reads your model and renders every attribute automatically — type, label, form widget, display formatter, table column. You only declare overrides:
25
+ Plutonium reads your model and renders every attribute automatically: type, label, form widget, display formatter, table column. You only declare overrides:
26
26
 
27
27
  ```ruby
28
28
  class PostDefinition < Plutonium::Resource::Definition
@@ -33,18 +33,20 @@ end
33
33
  ```
34
34
 
35
35
  ::: warning Don't declare for completeness
36
- A `field :title` with no options that matches what Plutonium would auto-detect is **dead code** — it does nothing and clutters the file. Declare ONLY when you need a different type, an option, a `condition:`, a block, or a custom component.
36
+ A `field :title` with no options that matches what Plutonium would auto-detect is **dead code**; it does nothing and clutters the file. Declare ONLY when you need a different type, an option, a `condition:`, a block, or a custom component.
37
37
  :::
38
38
 
39
39
  ## Sub-pages
40
40
 
41
- - [Model](./model) — `Plutonium::Resource::Record`, `has_cents`, SGID, custom routing, labeling
42
- - [Definition](./definition) — fields, inputs, displays, columns, page chrome, metadata panel, index views
43
- - [Query](./query) — search, filters, scopes, sorting
44
- - [Actions](./actions) — custom actions, bulk actions, interaction integration
41
+ - [Model](./model): `Plutonium::Resource::Record`, `has_cents`, SGID, custom routing, labeling
42
+ - [Definition](./definition): fields, inputs, displays, columns, page chrome, metadata panel, index views
43
+ - [Query](./query): search, filters, scopes, sorting
44
+ - [Actions](./actions): custom actions, bulk actions, interaction integration
45
+ - [Positioning & drag-to-reorder](./positioning): `positioned_on`, `position_on`, the drag grip, `reposition?`
46
+ - [CSV Export](./export): streamed export, opt-in through the policy
45
47
 
46
48
  ## Related
47
49
 
48
- - [Guides › Adding Resources](/guides/adding-resources) — task recipe
49
- - [App › Generators](/reference/app/generators) — `pu:res:scaffold` / `pu:res:conn` reference
50
- - [Tenancy](/reference/tenancy/) — multi-tenant scoping
50
+ - [Guides › Adding Resources](/guides/adding-resources): task recipe
51
+ - [App › Generators](/reference/app/generators): `pu:res:scaffold` / `pu:res:conn` reference
52
+ - [Tenancy](/reference/tenancy/): multi-tenant scoping
@@ -11,25 +11,25 @@ All resource models inherit from `ResourceRecord` (created by `pu:core:install`)
11
11
  class Post < ResourceRecord
12
12
  end
13
13
 
14
- # Inside a feature package — uses the package's ResourceRecord
14
+ # Inside a feature package: uses the package's ResourceRecord
15
15
  module Blogging
16
16
  class Post < Blogging::ResourceRecord
17
17
  end
18
18
  end
19
19
  ```
20
20
 
21
- `ResourceRecord` is abstract and inherits from `ApplicationRecord`. Standard ActiveRecord features (associations, validations, scopes, callbacks, attribute macros) all work — Plutonium adds capabilities on top.
21
+ `ResourceRecord` is abstract and inherits from `ApplicationRecord`. Standard ActiveRecord features (associations, validations, scopes, callbacks, attribute macros) all work; Plutonium adds capabilities on top.
22
22
 
23
23
  ## What `Plutonium::Resource::Record` adds
24
24
 
25
25
  | Module | Purpose | Section |
26
26
  |---|---|---|
27
- | `HasCents` | Money handling — cents column ↔ decimal accessor | [has_cents](#has-cents) |
27
+ | `HasCents` | Money handling: cents column ↔ decimal accessor | [has_cents](#has-cents) |
28
28
  | `Routes` | URL parameter customization (slugs, dynamic params) | [URL routing](#url-routing) |
29
29
  | `Labeling` | `to_label` for human-readable record names | [Labeling](#labeling) |
30
30
  | `FieldNames` | Field introspection by category | [Field introspection](#field-introspection) |
31
31
  | `Associations` | Auto-generated SGID accessors on every association | [SGID accessors](#sgid-accessors) |
32
- | `AssociatedWith` | Multi-tenant scoping — `Model.associated_with(entity)` | [Tenancy](/reference/tenancy/entity-scoping) |
32
+ | `AssociatedWith` | Multi-tenant scoping: `Model.associated_with(entity)` | [Tenancy](/reference/tenancy/entity-scoping) |
33
33
 
34
34
  ## Section layout
35
35
 
@@ -89,7 +89,7 @@ end
89
89
 
90
90
  ## `has_cents`
91
91
 
92
- Stores monetary values as integer cents and exposes a decimal virtual accessor. Use this for money — never store decimals directly.
92
+ Stores monetary values as integer cents and exposes a decimal virtual accessor. Use this for money; never store decimals directly.
93
93
 
94
94
  ```ruby
95
95
  class Product < ResourceRecord
@@ -112,16 +112,16 @@ product.price = 10.999
112
112
  product.price_cents # => 1099
113
113
  ```
114
114
 
115
- **Currency symbol (`unit:`)** — a `String` is used verbatim (`unit: "£"`); a `Symbol`
115
+ **Currency symbol (`unit:`).** A `String` is used verbatim (`unit: "£"`); a `Symbol`
116
116
  names a method read off the record for per-row currencies (`unit: :currency_symbol`
117
117
  → `record.currency_symbol`); `false` explicitly renders no symbol. This unit is picked
118
- up automatically anywhere the value renders as currency — show pages, tables, and
119
- grid/kanban cards — so you configure it once on the model. A per-display
118
+ up automatically anywhere the value renders as currency (show pages, tables, and
119
+ grid/kanban cards), so you configure it once on the model. A per-display
120
120
  `display :price, as: :currency, unit: …` overrides it for that display.
121
121
 
122
122
  A `has_cents` field also **infers the currency input** on forms: a bare `input :price`
123
123
  renders the [currency input](../ui/forms#currency-fields) (number field + unit prefix),
124
- no `as: :currency` needed — matching the display side.
124
+ no `as: :currency` needed, matching the display side.
125
125
 
126
126
  Resolution is `display unit → has_cents unit → config default`, where `nil` means
127
127
  "not set, keep looking" and `false` means "stop, no symbol". When nothing is set, it
@@ -148,7 +148,7 @@ end
148
148
  field :price, as: :decimal # ✅ virtual name
149
149
  ```
150
150
 
151
- Generators sometimes emit the `_cents` name in the policy — fix by hand (and verify `has_cents` is declared on the model).
151
+ Generators sometimes emit the `_cents` name in the policy; fix by hand (and verify `has_cents` is declared on the model).
152
152
  :::
153
153
 
154
154
  ### Options
@@ -176,7 +176,7 @@ product.errors[:price_cents] # => ["must be greater than 0"]
176
176
  product.errors[:price] # => ["is invalid"]
177
177
  ```
178
178
 
179
- The framework adds an `after_validation` hook that copies `:invalid` from `price_cents` → `price` automatically — no manual wiring needed.
179
+ The framework adds an `after_validation` hook that copies `:invalid` from `price_cents` → `price` automatically, no manual wiring needed.
180
180
 
181
181
  ### Introspection
182
182
 
@@ -196,7 +196,7 @@ post.to_param # => "1" (numeric id)
196
196
  # URL: /posts/1
197
197
  ```
198
198
 
199
- ### `path_parameter` — use a stable column
199
+ ### `path_parameter`: use a stable column
200
200
 
201
201
  Use a column that's unique and human-readable instead of the numeric id:
202
202
 
@@ -212,9 +212,9 @@ user.to_param # => "john_doe"
212
212
  User.from_path_param("john_doe") # finds by username
213
213
  ```
214
214
 
215
- `path_parameter` is a class-level macro (private class method). The column you pass MUST be unique — Plutonium uses it for lookup.
215
+ `path_parameter` is a class-level macro (private class method). The column you pass MUST be unique; Plutonium uses it for lookup.
216
216
 
217
- ### `dynamic_path_parameter` — SEO-friendly id + slug
217
+ ### `dynamic_path_parameter`: SEO-friendly id + slug
218
218
 
219
219
  Combines the id (for stable lookup) with a slug from another column (for SEO):
220
220
 
@@ -230,7 +230,7 @@ article.to_param # => "42-hello-world"
230
230
  Article.from_path_param("42-hello-world") # extracts "42", finds by id
231
231
  ```
232
232
 
233
- The slug is informational — only the id portion is used for lookup, so changing the title doesn't break old URLs.
233
+ The slug is informational: only the id portion is used for lookup, so changing the title doesn't break old URLs.
234
234
 
235
235
  ## Labeling
236
236
 
@@ -262,7 +262,7 @@ end
262
262
 
263
263
  ## SGID accessors
264
264
 
265
- Every association on a resource model gets Signed Global ID accessors automatically — for secure form submission, API payloads, and hidden fields without exposing database ids.
265
+ Every association on a resource model gets Signed Global ID accessors automatically, for secure form submission, API payloads, and hidden fields without exposing database ids.
266
266
 
267
267
  ### Singular associations (`belongs_to`, `has_one`)
268
268
 
@@ -292,7 +292,7 @@ user.add_post_sgid(sgid) # append
292
292
  user.remove_post_sgid(sgid) # remove
293
293
  ```
294
294
 
295
- These are what `secure_association_tag` uses in forms — see [UI › Forms](/reference/ui/forms).
295
+ These are what `secure_association_tag` uses in forms; see [UI › Forms](/reference/ui/forms).
296
296
 
297
297
  ## Field introspection
298
298
 
@@ -335,11 +335,11 @@ Resolution order, association path requirements, three model shapes, and custom
335
335
 
336
336
  ## Standard ActiveRecord features
337
337
 
338
- Everything you'd expect works — associations, validations, scopes, callbacks, delegations, `has_rich_text`, `has_secure_token`, `has_one_attached`, etc. Where Plutonium adds twists:
338
+ Everything you'd expect works: associations, validations, scopes, callbacks, delegations, `has_rich_text`, `has_secure_token`, `has_one_attached`, etc. Where Plutonium adds twists:
339
339
 
340
- - **Section ordering** is by convention, not enforcement — pick the right slot in the [layout above](#section-layout) so the file stays scannable.
341
- - **Compound uniqueness for tenant-scoped resources:** `validates :code, uniqueness: {scope: :organization_id}` — without the scope, uniqueness leaks across tenants.
342
- - **Keep models thin** — business logic that touches multiple records or has multi-step state changes belongs in [interactions](/reference/behavior/interactions), not model methods.
340
+ - **Section ordering** is by convention, not enforcement: pick the right slot in the [layout above](#section-layout) so the file stays scannable.
341
+ - **Compound uniqueness for tenant-scoped resources:** `validates :code, uniqueness: {scope: :organization_id}`, without the scope, uniqueness leaks across tenants.
342
+ - **Keep models thin**: business logic that touches multiple records or has multi-step state changes belongs in [interactions](/reference/behavior/interactions), not model methods.
343
343
 
344
344
  ## Nested resources
345
345
 
@@ -369,6 +369,6 @@ Override with `self.table_name = "posts"` if you need a shared table.
369
369
 
370
370
  ## Related
371
371
 
372
- - [Definition](./definition) — controls how the model's fields render
373
- - [Tenancy › Entity scoping](/reference/tenancy/entity-scoping) — `associated_with`, three model shapes
374
- - [App › Generators](/reference/app/generators) — `pu:res:scaffold` field syntax
372
+ - [Definition](./definition): controls how the model's fields render
373
+ - [Tenancy › Entity scoping](/reference/tenancy/entity-scoping): `associated_with`, three model shapes
374
+ - [App › Generators](/reference/app/generators): `pu:res:scaffold` field syntax