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
  # Kanban DSL Reference
2
2
 
3
3
  ::: warning Experimental
4
- Kanban boards are experimental — the DSL and behavior may change in a future release.
4
+ Kanban boards are experimental: the DSL and behavior may change in a future release.
5
5
  :::
6
6
 
7
7
  Complete reference for the `kanban do…end` block declared inside a resource Definition.
@@ -49,9 +49,9 @@ Default: `nil` (unlimited).
49
49
 
50
50
  Controls how card positions are persisted after a drag-and-drop. Three modes:
51
51
 
52
- **Inherited from the definition.** `position_on` is the same verb outside `kanban do…end`, where it makes the resource's [table and grid drag-reorderable](/reference/positioning). A board with no `position_on` of its own uses the **definition's**, falling back to the historic default (`:position`, Mode A). Declaring it inside the board overrides that. Resolution is lazy, so declaration order in the class body does not matter.
52
+ **Inherited from the definition.** `position_on` is the same verb outside `kanban do…end`, where it makes the resource's [table and grid drag-reorderable](/reference/resource/positioning). A board with no `position_on` of its own uses the **definition's**, falling back to the historic default (`:position`, Mode A). Declaring it inside the board overrides that. Resolution is lazy, so declaration order in the class body does not matter.
53
53
 
54
- #### Mode A — delegate to `Plutonium::Positioning::Model` (default)
54
+ #### Mode A: delegate to `Plutonium::Positioning::Model` (default)
55
55
 
56
56
  ```ruby
57
57
  # Default: uses :position attribute
@@ -64,30 +64,30 @@ position_on :sort_order
64
64
  Requires the model to:
65
65
  1. `include Plutonium::Positioning::Model`
66
66
  2. Call `positioned_on :position, scope: :grouping_attribute`
67
- 3. Have a `decimal` column for the position attribute — add it with the `t.position` migration helper (a tuned `decimal(16,8)`) — see [Positioning › Migration](/reference/kanban/positioning#migration)
67
+ 3. Have a `decimal` column for the position attribute: add it with the `t.position` migration helper (a tuned `decimal(16,8)`); see [Positioning › Migration](/reference/kanban/positioning#migration)
68
68
 
69
69
  On drop, calls `record.reposition!(prev_record:, next_record:)` which computes the decimal midpoint and updates the record.
70
70
 
71
- #### Mode B — BYO block
71
+ #### Mode B: BYO block
72
72
 
73
73
  ```ruby
74
74
  position_on :sort_order do |move|
75
75
  # move is a Plutonium::Positioning::Move value object
76
76
  # (also reachable as Plutonium::Kanban::Positioning::Move):
77
- # move.record — the dropped ActiveRecord record
78
- # move.column — destination column key (Symbol)
79
- # move.prev — record immediately before the insertion slot, or nil
80
- # move.next — record immediately after the insertion slot, or nil
81
- # move.index — 0-based insertion index within the destination column
77
+ # move.record : the dropped ActiveRecord record
78
+ # move.column : destination column key (Symbol)
79
+ # move.prev : record immediately before the insertion slot, or nil
80
+ # move.next : record immediately after the insertion slot, or nil
81
+ # move.index : 0-based insertion index within the destination column
82
82
  move.record.update!(sort_order: compute_position(move.prev, move.next))
83
83
  end
84
84
  ```
85
85
 
86
- The block is evaluated via `call` (not `instance_exec`) — it is a plain Ruby proc/lambda.
86
+ The block is evaluated via `call` (not `instance_exec`); it is a plain Ruby proc/lambda.
87
87
 
88
88
  Plutonium still orders column cards by `sort_order` (the first argument); your block is responsible only for persisting the new value.
89
89
 
90
- #### Mode C — disabled
90
+ #### Mode C: disabled
91
91
 
92
92
  ```ruby
93
93
  position_on false
@@ -114,9 +114,9 @@ Enables ActionCable broadcasting after every successful move. After a drop, Plut
114
114
  - Tenant-scoped portals use the entity's Global ID parameter as the second segment.
115
115
  - Portals without entity scoping use the literal string `"global"`.
116
116
 
117
- Two viewers share a stream only if they have the same resource class **and** the same scoped entity — cross-tenant leakage is impossible by construction.
117
+ Two viewers share a stream only if they have the same resource class **and** the same scoped entity; cross-tenant leakage is impossible by construction.
118
118
 
119
- Requires `turbo-rails` + ActionCable (gems), a cable adapter in `config/cable.yml` (Redis/Solid Cable in multi-process production), ActionCable mounted at `/cable`, **and** an ActionCable client loaded in your app's JavaScript. Plutonium's bundle ships `@hotwired/turbo` only — without `@hotwired/turbo-rails` (or `@rails/actioncable`) in your pack, the `<turbo-cable-stream-source>` never connects and other viewers won't update. Server-side broadcasting works regardless; this is purely the client subscription. See the [guide's Realtime setup](/guides/kanban#setup-required-for-realtime-to-actually-update-other-viewers).
119
+ Requires `turbo-rails` + ActionCable (gems), a cable adapter in `config/cable.yml` (Redis/Solid Cable in multi-process production), ActionCable mounted at `/cable`, **and** an ActionCable client loaded in your app's JavaScript. Plutonium's bundle ships `@hotwired/turbo` only; without `@hotwired/turbo-rails` (or `@rails/actioncable`) in your pack, the `<turbo-cable-stream-source>` never connects and other viewers won't update. Server-side broadcasting works regardless; this is purely the client subscription. See the [guide's Realtime setup](/guides/kanban#setup-required-for-realtime-to-actually-update-other-viewers).
120
120
 
121
121
  Default: `false`.
122
122
 
@@ -141,14 +141,14 @@ show_in :modal # open a card's show page in a centered modal dialog
141
141
  show_in :page # navigate the whole page to the show route
142
142
  ```
143
143
 
144
- Overrides — **for this board** — where clicking a card opens the record's show page:
144
+ Overrides, **for this board**, where clicking a card opens the record's show page:
145
145
 
146
- - `:modal` — the card's show link targets the layout's `remote_modal` frame, so the show page renders in a **centered** dialog. (Show is always centered — deliberately not the definition's `modal_mode`, which styles `new`/`edit`.)
147
- - `:page` — the card's show link targets `_top`, navigating the whole page to the show route.
146
+ - `:modal`: the card's show link targets the layout's `remote_modal` frame, so the show page renders in a **centered** dialog. (Show is always centered, deliberately not the definition's `modal_mode`, which styles `new`/`edit`.)
147
+ - `:page`: the card's show link targets `_top`, navigating the whole page to the show route.
148
148
 
149
149
  When `show_in` is **not** set on the board, the board inherits the definition's [`show_in`](/reference/resource/definition#show_in) (which itself defaults to `:page`). So to open cards in a modal you can set it on the board, or once on the definition (which also covers the table and grid views).
150
150
 
151
- Either mode escapes the column's lazy turbo-frame — `:page` replaces the whole page, and the `remote_modal` frame lives in the layout (resolved document-wide), so it opens outside the column. No per-card configuration is needed; the show page detects the modal frame (`in_modal?`) and wraps its details in the centered modal chrome. From inside the modal, an expand icon (or ⌘/Ctrl-click on the card) opens the full page in a new tab.
151
+ Either mode escapes the column's lazy turbo-frame: `:page` replaces the whole page, and the `remote_modal` frame lives in the layout (resolved document-wide), so it opens outside the column. No per-card configuration is needed; the show page detects the modal frame (`in_modal?`) and wraps its details in the centered modal chrome. From inside the modal, an expand icon (or ⌘/Ctrl-click on the card) opens the full page in a new tab.
152
152
 
153
153
  An unknown mode raises `ArgumentError`.
154
154
 
@@ -168,7 +168,7 @@ Overrides the slot layout for every kanban card on this board, using the same sl
168
168
 
169
169
  When `card_fields` is not set, cards fall back to the resource definition's `grid_fields`. If neither is declared, the card renders the default header-only layout.
170
170
 
171
- The `meta` slot renders each field as a colored badge, and formats values by type before badging: a `has_cents` field renders as currency, a `belongs_to` association renders as its label (not an object inspect), and everything else is humanized — with status-like enums (`active`, `pending`, `published`…) resolving to a semantic color. The badge color is deterministic per value, so a given status is the same color on every card.
171
+ The `meta` slot renders each field as a colored badge, and formats values by type before badging: a `has_cents` field renders as currency, a `belongs_to` association renders as its label (not an object inspect), and everything else is humanized, with status-like enums (`active`, `pending`, `published`…) resolving to a semantic color. The badge color is deterministic per value, so a given status is the same color on every card.
172
172
 
173
173
  ---
174
174
 
@@ -203,12 +203,12 @@ end
203
203
  | `color:` | Symbol or String | `nil` | Header color dot. Named colors: `:red`, `:orange`, `:amber`, `:yellow`, `:green`, `:blue`, `:purple`, `:pink`, `:gray`. Raw CSS string also accepted |
204
204
  | `scope:` | Symbol or Proc | `nil` | Relation filter for this column. **Symbol** → `relation.public_send(sym)` (named AR scope). **Proc** → 0-arg lambda called via `instance_exec` on the relation, e.g. `-> { where(status: "todo") }` |
205
205
  | `on_enter:` | Symbol or Proc | `nil` | Fired when a card is dropped into this column. **Symbol** → `record.public_send(sym)`. **Proc** → 1-arg lambda `->(record) { … }` where `self` inside the block is the view context (giving access to `current_user`, helpers, etc.). The callback may assign attributes in memory (`r.status = :done`) or call `update!` directly; if the record has unsaved changes after `on_enter` returns the controller saves it automatically. |
206
- | `on_exit:` | Symbol or Proc | `nil` | The source-side counterpart to `on_enter:`, fired when a card **leaves** this column on a cross-column move. Same dispatch (**Symbol** → `record.public_send`; **Proc** → 1-arg lambda, `self` = view context). Runs **before** the destination's `on_enter`, inside the same move transaction, so it sees the pre-move state and rolls back if the move fails. Use it for source-tied side effects the destination can't own (stop a timer, release a slot). Fires only on a drag-move through `kanban_move` — not on destroy, a programmatic status change, or quick-add. Skipped on same-column reorders. |
206
+ | `on_exit:` | Symbol or Proc | `nil` | The source-side counterpart to `on_enter:`, fired when a card **leaves** this column on a cross-column move. Same dispatch (**Symbol** → `record.public_send`; **Proc** → 1-arg lambda, `self` = view context). Runs **before** the destination's `on_enter`, inside the same move transaction, so it sees the pre-move state and rolls back if the move fails. Use it for source-tied side effects the destination can't own (stop a timer, release a slot). Fires only on a drag-move through `kanban_move`, not on destroy, a programmatic status change, or quick-add. Skipped on same-column reorders. Use a model callback instead only when other save paths (edit form, API, import) must be covered too, and then drop the `on_exit:` so the work isn't done twice. |
207
207
  | `enter_interaction:` | Class | `nil` | A **record-scoped** interaction class (declares `attribute :resource`) run when a card is dropped **into** this column from another column. Opens the interaction's form as a modal to collect input, then commits `on_enter` + the interaction + repositioning atomically. Auto-registered as a hidden record action under a column-scoped key (`:<column>_enter_interaction`), authorized by `kanban_move?` (no policy method of its own). See [enter_interaction](#drop-interaction) below |
208
208
  | `role:` | `:backlog`, `:done`, `:lost` | `nil` | Applies a preset (see below) |
209
209
  | `collapsed:` | Boolean | `false` | Column starts collapsed (a thin strip with the label rotated). The Stimulus controller persists the toggled state to `localStorage` (key: `pu-kanban:<path>:<column-key>:collapsed`) so the user preference survives page reloads; this DSL value sets the server-rendered initial state only. |
210
210
  | `add:` | Boolean | `false` | Show a `+ Add` quick-add button |
211
- | `accepts:` | `true`, `false`, or Array | `true` | Drop policy — **structural topology only**. `true` accepts any source column. `false` rejects all drops (display-only column). An Array of column key symbols accepts only those sources. This is client-hintable: the value is emitted as `data-kanban-accepts` so the drag UI greys out disallowed source columns before the drop. **Record- or user-conditional rules do not belong here** — put them in `kanban_move?`, which sees the record and the `from`/`to` columns (see [Authorization](./authorization)). Passing a `Proc` raises `ArgumentError`. |
211
+ | `accepts:` | `true`, `false`, or Array | `true` | Drop policy: **structural topology only**. `true` accepts any source column. `false` rejects all drops (display-only column). An Array of column key symbols accepts only those sources. This is client-hintable: the value is emitted as `data-kanban-accepts` so the drag UI greys out disallowed source columns before the drop. **Record- or user-conditional rules do not belong here**. Put them in `kanban_move?`, which sees the record and the `from`/`to` columns (see [Authorization](./authorization)). Passing a `Proc` raises `ArgumentError`. |
212
212
  | `locked:` | Boolean | `false` | Prevent dragging cards **out of** this column |
213
213
  | `wip:` | Integer | `nil` | WIP limit. Reject cross-column drops when `dest_count + 1 > wip`. Has no effect on same-column reordering |
214
214
 
@@ -219,7 +219,7 @@ The old names are **deprecated aliases**:
219
219
  - In **development and test** they raise an `ArgumentError` so the rename is caught before release.
220
220
  - In **deployed environments** (production, staging) they log a deprecation warning and map onto the new option, so an in-flight deployment keeps working across the upgrade.
221
221
 
222
- If both the old and new name are given, the new one wins. Update your definitions to `on_enter:` / `enter_interaction:` at your earliest convenience — a future release will drop the aliases entirely.
222
+ If both the old and new name are given, the new one wins. Update your definitions to `on_enter:` / `enter_interaction:` at your earliest convenience; a future release will drop the aliases entirely.
223
223
  :::
224
224
 
225
225
  ### Role presets
@@ -230,7 +230,7 @@ If both the old and new name are given, the new one wins. Update your definition
230
230
  | `:done` | `color: :green, collapsed: true` |
231
231
  | `:lost` | `color: :red, collapsed: true` |
232
232
 
233
- `:done` and `:lost` are the two terminal roles — both collapsed by default, the
233
+ `:done` and `:lost` are the two terminal roles, both collapsed by default, the
234
234
  colour signalling the outcome (`:done` = positive close, `:lost` = negative
235
235
  close). Use them as the won/lost pair in pipelines (leads, deals, tickets).
236
236
 
@@ -246,13 +246,13 @@ column :lost,
246
246
 
247
247
  Runs an authorization-aware, input-collecting interaction when a card is dropped **into** this column from another column.
248
248
 
249
- - **Must be a record-scoped interaction** — the class declares `attribute :resource` (singular) and acts on the one dropped card. A `resources`-plural (bulk) interaction is not valid here; that shape is for [column actions](#column-actions).
250
- - **Auto-registered as a hidden record action** under a column-scoped key (`:lost` → `:lost_enter_interaction`) — unique by construction, so two columns can reuse the same interaction class without colliding. Hidden = it does not render as an action button on the show page, table rows, or grid cards; it is reachable only by a drop.
251
- - **Authorized by `kanban_move?` alone** — the interaction has **no policy method of its own**. The move (and therefore the interaction) is gated by the single `kanban_move?` predicate, which can gate this specific transition via its `to` column context (e.g. `def kanban_move? = kanban_to&.key == :lost ? user.manager? : super`). See [Authorization](./authorization).
252
- - **Move flow only.** Dropping cross-column opens the interaction's form as a modal; on submit `on_enter` + the interaction + repositioning commit in **one atomic transaction**. Validation failure rolls the whole transaction back (membership write included) and re-renders the modal with errors — nothing persists. Same-column reorders run positioning only (neither `on_enter` nor the interaction fires).
253
- - **Quick-add (`+ Add`)** applies `on_enter` + positioning **post-create** — the record is created first (needs a grouping-column default), then `on_enter` places it in the column; the `enter_interaction` is not involved.
254
- - **Author contract** — when a column declares both, `on_enter` owns the membership attribute (e.g. `status`) and the interaction owns the extras (reason, mail, audit). If the interaction also writes the membership attribute it must set the same value `on_enter` does (idempotent). With no `on_enter`, the interaction owns everything.
255
- - **Success response limitation** — the interaction's success **message** (`.with_message`) surfaces as a toast, but a custom success *response* (`with_redirect_response`, `with_file_response`, …) is **not** honored on the drop path; the board just re-renders and closes the modal.
249
+ - **Must be a record-scoped interaction.** The class declares `attribute :resource` (singular) and acts on the one dropped card. A `resources`-plural (bulk) interaction is not valid here; that shape is for [column actions](#column-actions).
250
+ - **Auto-registered as a hidden record action** under a column-scoped key (`:lost` → `:lost_enter_interaction`), unique by construction, so two columns can reuse the same interaction class without colliding. Hidden = it does not render as an action button on the show page, table rows, or grid cards; it is reachable only by a drop.
251
+ - **Authorized by `kanban_move?` alone.** The interaction has **no policy method of its own**. The move (and therefore the interaction) is gated by the single `kanban_move?` predicate, which can gate this specific transition via its `to` column context (e.g. `def kanban_move? = kanban_to&.key == :lost ? user.manager? : super`). See [Authorization](./authorization).
252
+ - **Move flow only.** Dropping cross-column opens the interaction's form as a modal; on submit `on_enter` + the interaction + repositioning commit in **one atomic transaction**. Validation failure rolls the whole transaction back (membership write included) and re-renders the modal with errors; nothing persists. Same-column reorders run positioning only (neither `on_enter` nor the interaction fires).
253
+ - **Quick-add (`+ Add`)** applies `on_enter` + positioning **post-create**: the record is created first (needs a grouping-column default), then `on_enter` places it in the column; the `enter_interaction` is not involved.
254
+ - **Author contract.** When a column declares both, `on_enter` owns the membership attribute (e.g. `status`) and the interaction owns the extras (reason, mail, audit). If the interaction also writes the membership attribute it must set the same value `on_enter` does (idempotent). With no `on_enter`, the interaction owns everything.
255
+ - **Success response limitation.** The interaction's success **message** (`.with_message`) surfaces as a toast, but a custom success *response* (`with_redirect_response`, `with_file_response`, …) is **not** honored on the drop path; the board just re-renders and closes the modal.
256
256
 
257
257
  See the [guide's Interaction on drop section](/guides/kanban#interaction-on-drop) for a full worked example.
258
258
 
@@ -265,7 +265,7 @@ Use `columns do…end` when the column list depends on the current request:
265
265
  ```ruby
266
266
  kanban do
267
267
  columns do
268
- # `self` is the view context — current_user, params, and helpers all work.
268
+ # `self` is the view context: current_user, params, and helpers all work.
269
269
  current_user.visible_statuses.map do |status|
270
270
  Plutonium::Kanban::Column.new(
271
271
  :"status_#{status.id}",
@@ -299,7 +299,7 @@ class TaskDefinition < ResourceDefinition
299
299
  end
300
300
  ```
301
301
 
302
- **`enter_interaction:` is not supported on dynamic boards.** Unlike a column action, its hidden action can't be registered manually — the key is column-scoped and internal — and the static registration pass has no columns to see. A drop into a dynamic column that declares `enter_interaction:` is rejected with a snap-back (it does not crash). Use a static board when a column needs an `enter_interaction:`.
302
+ **`enter_interaction:` is not supported on dynamic boards.** Unlike a column action, its hidden action can't be registered manually: the key is column-scoped and internal, and the static registration pass has no columns to see. A drop into a dynamic column that declares `enter_interaction:` is rejected with a snap-back (it does not crash). Use a static board when a column needs an `enter_interaction:`.
303
303
  :::
304
304
 
305
305
  ---
@@ -326,7 +326,7 @@ end
326
326
 
327
327
  | Option | Type | Required | Description |
328
328
  |--------|------|----------|-------------|
329
- | `interaction:` | Class | Yes | An interaction class. Must have `attribute :resources` (plural) — it runs as a bulk action |
329
+ | `interaction:` | Class | Yes | An interaction class. Must have `attribute :resources` (plural); it runs as a bulk action |
330
330
  | `on:` | `:all` or `:visible` | No (default `:all`) | `:all` passes IDs of all column cards (ignoring `per_column`). `:visible` passes only the rendered, capped subset |
331
331
  | `label:` | String | No | Button text. Defaults to `key.to_s.humanize` |
332
332
  | `icon:` | Phlex icon class | No | Icon rendered before the label |
@@ -6,7 +6,7 @@ Reference documentation for the Plutonium kanban board feature.
6
6
 
7
7
  | Page | What it covers |
8
8
  |------|---------------|
9
- | [DSL](/reference/kanban/dsl) | Complete `kanban do…end` DSL — board options, columns, column actions, static vs. dynamic, lazy loading, realtime |
9
+ | [DSL](/reference/kanban/dsl) | Complete `kanban do…end` DSL: board options, columns, column actions, static vs. dynamic, lazy loading, realtime |
10
10
  | [Positioning](/reference/kanban/positioning) | `Plutonium::Positioning::Model` concern, `positioned_on`, `position_on` modes, `reposition!`, rebalancing |
11
11
  | [Authorization](/reference/kanban/authorization) | `kanban_move?` policy predicate, read-only fallback, separating move rights from edit rights |
12
12
 
@@ -1,10 +1,10 @@
1
1
  # Kanban Positioning
2
2
 
3
3
  ::: tip Positioning is not kanban-only
4
- The model concern, the arithmetic and the `position_on` modes on this page are shared with **table and grid drag-to-reorder** — see [Positioning & drag-to-reorder](/reference/positioning) for the index-surface half of the feature (the grip, the `reposition` endpoint, `reposition?`, and board inheritance).
4
+ The model concern, the arithmetic and the `position_on` modes on this page are shared with **table and grid drag-to-reorder**; see [Positioning & drag-to-reorder](/reference/resource/positioning) for the index-surface half of the feature (the grip, the `reposition` endpoint, `reposition?`, and board inheritance).
5
5
  :::
6
6
 
7
- Plutonium uses **decimal fractional positioning** for kanban card ordering. A drop writes a single decimal position (the midpoint between its neighbors), so the common case touches exactly one row — no bulk renumbering. The one exception is rare **rebalancing**: when the same slot has been subdivided ~20 times and the gap between two neighbors shrinks below `1e-6`, Plutonium renumbers that one scope group back to clean integers before inserting (see [Gap exhaustion](#rebalancing)).
7
+ Plutonium uses **decimal fractional positioning** for kanban card ordering. A drop writes a single decimal position (the midpoint between its neighbors), so the common case touches exactly one row, with no bulk renumbering. The one exception is rare **rebalancing**: when the same slot has been subdivided ~20 times and the gap between two neighbors shrinks below `1e-6`, Plutonium renumbers that one scope group back to clean integers before inserting (see [Gap exhaustion](#rebalancing)).
8
8
 
9
9
  ## `Plutonium::Positioning::Model` concern
10
10
 
@@ -34,7 +34,7 @@ After calling `positioned_on`, the model gets:
34
34
 
35
35
  ### Migration
36
36
 
37
- Use the **`t.position`** helper — it adds a `decimal` column already tuned for fractional ordering (`precision: 16, scale: 8`), so you can't get the scale wrong:
37
+ Use the **`t.position`** helper: it adds a `decimal` column already tuned for fractional ordering (`precision: 16, scale: 8`), so you can't get the scale wrong:
38
38
 
39
39
  ```ruby
40
40
  create_table :tasks do |t|
@@ -56,6 +56,8 @@ class AddPositionToTasks < ActiveRecord::Migration[8.1]
56
56
  end
57
57
  ```
58
58
 
59
+ For a model owned by a feature package (e.g. `Catalog::Product`), put this migration in `packages/<package>/db/migrate/`, next to the table's create migration. Each package's `db/migrate` is appended to the app's migration paths, so `rails db:migrate` picks it up. Plain `rails g migration` always writes to the main app's `db/migrate`, so move the file into the package afterwards.
60
+
59
61
  `t.position` accepts a custom column name and any `column` options:
60
62
 
61
63
  ```ruby
@@ -65,7 +67,7 @@ t.position :position, scale: 10 # override precision/scale
65
67
  ```
66
68
 
67
69
  ::: tip Why the helper picks `scale: 8`
68
- If you write the column by hand, give it at least **two more decimal places than `EPSILON` (`1e-6`)** — i.e. `scale: 8` or higher. Rebalancing triggers when a gap drops below `1e-6`, so a column that can store smaller values still has room to write the final midpoint cleanly. A `scale: 6` column has no headroom: the last subdivision before a rebalance can round to a neighbor and momentarily collide. `t.position` defaults to `scale: 8`, which is safe.
70
+ If you write the column by hand, give it at least **two more decimal places than `EPSILON` (`1e-6`)**, i.e. `scale: 8` or higher. Rebalancing triggers when a gap drops below `1e-6`, so a column that can store smaller values still has room to write the final midpoint cleanly. A `scale: 6` column has no headroom: the last subdivision before a rebalance can round to a neighbor and momentarily collide. `t.position` defaults to `scale: 8`, which is safe.
69
71
  :::
70
72
 
71
73
  ---
@@ -88,7 +90,7 @@ task.reposition!(prev_record: last_card, next_record: nil) # append
88
90
 
89
91
  ### Gap exhaustion (rebalancing) {#rebalancing}
90
92
 
91
- Each midpoint insert into the *same* slot halves the gap (`1.0 → 0.5 → 0.25 → …`), so after roughly 20 consecutive insertions the gap drops below `EPSILON` (`1e-6`). At that point `reposition!` rebalances **only that scope group** — renumbering every row in the group to fresh integers (`1.0, 2.0, 3.0, …`) in current-position order, inside a transaction — then reloads the two neighbors and writes the new midpoint. Other scope groups are untouched. End moves (a `nil` neighbor) never rebalance: they always have integer room via `prev ± 1`.
93
+ Each midpoint insert into the *same* slot halves the gap (`1.0 → 0.5 → 0.25 → …`), so after roughly 20 consecutive insertions the gap drops below `EPSILON` (`1e-6`). At that point `reposition!` rebalances **only that scope group**, renumbering every row in the group to fresh integers (`1.0, 2.0, 3.0, …`) in current-position order, inside a transaction, then reloads the two neighbors and writes the new midpoint. Other scope groups are untouched. End moves (a `nil` neighbor) never rebalance: they always have integer room via `prev ± 1`.
92
94
 
93
95
  ---
94
96
 
@@ -108,7 +110,7 @@ Task.backfill_positions!(order: :created_at)
108
110
  The `position_on` call inside `kanban do…end` controls how Plutonium persists positions after a drag-and-drop. Three modes are available:
109
111
 
110
112
  ::: tip A board inherits the definition's `position_on`
111
- `position_on` is the **same verb** at both levels. A board resolves its strategy as: its own `position_on`, else the **definition's**, else the historic default (`:position`, Mode A). So a resource whose definition already declares `position_on` for its table and grid needs nothing inside `kanban do…end` — the board picks up the same attribute and the same mode.
113
+ `position_on` is the **same verb** at both levels. A board resolves its strategy as: its own `position_on`, else the **definition's**, else the historic default (`:position`, Mode A). So a resource whose definition already declares `position_on` for its table and grid needs nothing inside `kanban do…end`; the board picks up the same attribute and the same mode.
112
114
 
113
115
  Resolution is lazy, so a `kanban do…end` written **above** `position_on` in the class body still sees it.
114
116
 
@@ -123,7 +125,7 @@ end
123
125
  ```
124
126
  :::
125
127
 
126
- ### Mode A — delegate (default)
128
+ ### Mode A: delegate (default)
127
129
 
128
130
  ```ruby
129
131
  kanban do
@@ -135,26 +137,26 @@ end
135
137
 
136
138
  On drop, Plutonium calls `record.reposition!(prev_record:, next_record:)`. Requires the model to include `Plutonium::Positioning::Model` and call `positioned_on`.
137
139
 
138
- ### Mode B — BYO block
140
+ ### Mode B: BYO block
139
141
 
140
142
  ```ruby
141
143
  kanban do
142
144
  position_on :sort_order do |move|
143
- # move.record — the dropped record
144
- # move.column — destination column key (Symbol)
145
- # move.prev — record immediately before the slot (or nil)
146
- # move.next — record immediately after the slot (or nil)
147
- # move.index — 0-based insertion index within the destination column
145
+ # move.record: the dropped record
146
+ # move.column: destination column key (Symbol)
147
+ # move.prev: record immediately before the slot (or nil)
148
+ # move.next: record immediately after the slot (or nil)
149
+ # move.index: 0-based insertion index within the destination column
148
150
  move.record.update!(sort_order: my_position(move.prev, move.next))
149
151
  end
150
152
  end
151
153
  ```
152
154
 
153
- Plutonium orders the column by `sort_order` for display; your block is responsible only for persisting the new value. The block is called with a single `Plutonium::Positioning::Move` argument (still reachable under its original name, `Plutonium::Kanban::Positioning::Move`) — it is NOT `instance_exec`'d, so `self` is the proc's original binding.
155
+ Plutonium orders the column by `sort_order` for display; your block is responsible only for persisting the new value. The block is called with a single `Plutonium::Positioning::Move` argument (still reachable under its original name, `Plutonium::Kanban::Positioning::Move`); it is NOT `instance_exec`'d, so `self` is the proc's original binding.
154
156
 
155
- On a table or grid the same block runs with `move.column` set to `nil`, since those surfaces have no columns. See [Mode B](/reference/positioning#mode-b) for a worked `acts_as_list` example.
157
+ On a table or grid the same block runs with `move.column` set to `nil`, since those surfaces have no columns. See [Mode B](/reference/resource/positioning#mode-b) for a worked `acts_as_list` example.
156
158
 
157
- ### Mode C — disabled
159
+ ### Mode C: disabled
158
160
 
159
161
  ```ruby
160
162
  kanban do
@@ -1,32 +1,32 @@
1
1
  # Actions
2
2
 
3
- Custom buttons that go beyond standard CRUD — publish, archive, import, send invitation, etc. Two flavors:
3
+ Custom buttons that go beyond standard CRUD: publish, archive, import, send invitation, etc. Two flavors:
4
4
 
5
- - **Simple actions** — navigate to an existing URL.
6
- - **Interactive actions** — run an [Interaction](/reference/behavior/interactions), optionally collecting input via a modal form.
5
+ - **Simple actions**: navigate to an existing URL.
6
+ - **Interactive actions**: run an [Interaction](/reference/behavior/interactions), optionally collecting input via a modal form.
7
7
 
8
8
  ## 🚨 Critical
9
9
 
10
10
  - **Every custom action needs a policy method.** `action :publish` requires `def publish?` on the policy. Undefined methods return `false`, so the action silently disappears.
11
11
  - **For interactive actions, visibility is inferred from the interaction's attributes.** Don't declare `record_action: true` / `bulk_action: true` etc. by hand unless you're opting OUT.
12
12
  - **Bulk action authorization is per-record.** If any selected record fails the policy check, the entire request is rejected.
13
- - **Always pass `as:`** on custom routes — without it, `resource_url_for` can't generate URLs (critical for nested resources).
13
+ - **Always pass `as:`** on custom routes: without it, `resource_url_for` can't generate URLs (critical for nested resources).
14
14
  - **Prefer interactive actions over hand-written controller routes.** Anything a user triggers from a page belongs behind an interaction.
15
- - **An interaction is the button, not the operation.** Logic may start in `execute`; once a job or an API also needs it, it moves to the model — see [Behavior › Interactions](/reference/behavior/interactions#what-an-interaction-is-for).
15
+ - **An interaction is the button, not the operation.** Logic may start in `execute`; once a job or an API also needs it, it moves to the model, see [Behavior › Interactions](/reference/behavior/interactions#what-an-interaction-is-for).
16
16
 
17
17
  ## Action visibility flags
18
18
 
19
19
  | Flag | Where the button appears |
20
20
  |---|---|
21
- | `resource_action: true` | Index page (top toolbar) — for actions that operate on the collection (Import, Export, Create) |
22
- | `record_action: true` | Show page — for actions on a single record (Edit, Archive, Delete) |
23
- | `collection_record_action: true` | Per-row in the index table — for quick actions (Edit, Show) |
21
+ | `resource_action: true` | Index page (top toolbar): for actions that operate on the collection (Import, Export, Create) |
22
+ | `record_action: true` | Show page: for actions on a single record (Edit, Archive, Delete) |
23
+ | `collection_record_action: true` | Per-row in the index table: for quick actions (Edit, Show) |
24
24
  | `bulk_action: true` | Bulk-actions toolbar (shown when records are selected) |
25
- | `hidden: true` | **Nowhere.** Suppresses all four surfaces at once, while keeping the route and policy live — see [Hidden actions](#hidden-actions) |
25
+ | `hidden: true` | **Nowhere.** Suppresses all four surfaces at once, while keeping the route and policy live, see [Hidden actions](#hidden-actions) |
26
26
 
27
27
  ### Inferred visibility (interactive actions)
28
28
 
29
- For `interaction:`-based actions, all four flags are **inferred from the interaction's attributes** — don't declare them by hand:
29
+ For `interaction:`-based actions, all four flags are **inferred from the interaction's attributes**; don't declare them by hand:
30
30
 
31
31
  | Interaction declares | Inferred flags |
32
32
  |---|---|
@@ -34,7 +34,7 @@ For `interaction:`-based actions, all four flags are **inferred from the interac
34
34
  | `attribute :resources` (plural) | `bulk_action: true` |
35
35
  | neither | `resource_action: true` |
36
36
 
37
- User-supplied flags override the inferred ones, but only **opt-out** makes sense — the interaction's `attribute :resource` / `attribute :resources` already fixes its semantic shape:
37
+ User-supplied flags override the inferred ones, but only **opt-out** makes sense: the interaction's `attribute :resource` / `attribute :resources` already fixes its semantic shape:
38
38
 
39
39
  ```ruby
40
40
  # :resource interaction → defaults to record_action + collection_record_action.
@@ -63,10 +63,10 @@ action :name,
63
63
  collection_record_action: true,
64
64
  bulk_action: true,
65
65
 
66
- # Conditional visibility — display-only proc, NOT authorization (see below)
66
+ # Conditional visibility: display-only proc, NOT authorization (see below)
67
67
  condition: -> { params[:beta] == "1" },
68
68
 
69
- # Never render, anywhere — route + policy stay live (see below)
69
+ # Never render, anywhere: route + policy stay live (see below)
70
70
  hidden: true,
71
71
 
72
72
  # Grouping
@@ -78,19 +78,19 @@ action :name,
78
78
  turbo_frame: "_top",
79
79
  return_to: "/custom/path",
80
80
  route_options: {action: :foo},
81
- modal: :slideover, # :slideover / :centered — overrides the definition's modal mode
82
- size: :lg, # :sm / :md / :lg / :xl / :auto / :full — overrides the definition's modal size
81
+ modal: :slideover, # :slideover / :centered; overrides the definition's modal mode
82
+ size: :lg, # :sm / :md / :lg / :xl / :auto / :full; overrides the definition's modal size
83
83
 
84
84
  # HTML attributes (see below)
85
85
  link: {target: "_blank", rel: "noopener"}, # merged onto the action's <a> renderings
86
86
  button: {data: {analytics: "archive"}} # merged onto the button_to <form> (non-GET)
87
87
  ```
88
88
 
89
- ### HTML attributes — `link:` / `button:`
89
+ ### HTML attributes: `link:` / `button:`
90
90
 
91
- Two per-element attribute bags, deep-merged over the framework's own attributes at render time — **the author wins on every key**, recursively through nested `data`:
91
+ Two per-element attribute bags, deep-merged over the framework's own attributes at render time; **the author wins on every key**, recursively through nested `data`:
92
92
 
93
- - **`link:`** applies to every `<a>` rendered for the action: the toolbar link (GET), dropdown items (**any** HTTP method — dropdown items are always anchors, submitting via `data-turbo-method`), bulk-action links, kanban column action links, and the grid/kanban card's hidden show link (for `:show`).
93
+ - **`link:`** applies to every `<a>` rendered for the action: the toolbar link (GET), dropdown items (**any** HTTP method, dropdown items are always anchors, submitting via `data-turbo-method`), bulk-action links, kanban column action links, and the grid/kanban card's hidden show link (for `:show`).
94
94
  - **`button:`** applies to the `button_to` **`<form>`** element of the non-GET toolbar rendering (the form wrapper, not the inner `<button>`).
95
95
 
96
96
  ```ruby
@@ -100,14 +100,14 @@ action :documentation,
100
100
  link: {target: "_blank", rel: "noopener noreferrer", data: {analytics: "docs"}}
101
101
  ```
102
102
 
103
- Because the author wins, you can override anything — `turbo_frame`, `class`, `data-*` — at your own risk. Two things to know:
103
+ Because the author wins, you can override anything (`turbo_frame`, `class`, `data-*`) at your own risk. Two things to know:
104
104
 
105
- - `class:` **replaces** the framework's classes (no token append) — a bare `link: {class: "mt-2"}` removes the button styling entirely.
105
+ - `class:` **replaces** the framework's classes (no token append): a bare `link: {class: "mt-2"}` removes the button styling entirely.
106
106
  - Pass `data:` as a **hash**. The merge only recurses when both sides are hashes, so a scalar `data:` replaces the framework's data wholesale (dropping `turbo_confirm`/`turbo_frame`).
107
107
 
108
108
  Both bags round-trip through [`with(...)`](#deriving-variants-action-with), so `defined_actions[:edit].with(link: {target: "_blank"})` works in `customize_actions`.
109
109
 
110
- ### Deriving variants — `Action#with(...)`
110
+ ### Deriving variants: `Action#with(...)`
111
111
 
112
112
  Action records are frozen value objects. Inside `customize_actions`, derive a copy with overrides:
113
113
 
@@ -117,11 +117,11 @@ def customize_actions
117
117
  end
118
118
  ```
119
119
 
120
- ## Conditional visibility — `condition:` {#conditional-visibility}
120
+ ## Conditional visibility: `condition:` {#conditional-visibility}
121
121
 
122
- Like the `condition:` proc on [inputs/displays/columns](/reference/resource/definition), an action can be **defined but only rendered when a runtime proc is truthy**. It's purely a toggle on whether the **button is shown** — the action (and its route) stays fully live either way.
122
+ Like the `condition:` proc on [inputs/displays/columns](/reference/resource/definition), an action can be **defined but only rendered when a runtime proc is truthy**. It's purely a toggle on whether the **button is shown**; the action (and its route) stays fully live either way.
123
123
 
124
- The headline use case: **expose an action's endpoint without surfacing it in the UI** — e.g. one you call from the API, a webhook, or another service. Hide the button with an always-falsy condition; the route still works:
124
+ The headline use case: **expose an action's endpoint without surfacing it in the UI**, e.g. one you call from the API, a webhook, or another service. Hide the button with an always-falsy condition; the route still works:
125
125
 
126
126
  ```ruby
127
127
  # Defined and callable (API / programmatic), but no button anywhere in the UI:
@@ -133,7 +133,7 @@ It also works as a dynamic toggle driven by the **record** or the **view/request
133
133
  ```ruby
134
134
  # object → the row/shown record (record & collection-record actions):
135
135
  action :reopen, interaction: ReopenInteraction, condition: -> { object.closed? }
136
- # view/request state — feature flag, preview/beta mode:
136
+ # view/request state: feature flag, preview/beta mode:
137
137
  action :preview, interaction: PreviewInteraction, condition: -> { params[:beta] == "1" }
138
138
  ```
139
139
 
@@ -144,34 +144,34 @@ Inside the proc, `object`/`record` is the contextual record, and every other cal
144
144
  | `object` / `record` | The row/shown record for **record** and **collection-record** actions; **`nil`** for resource and bulk actions (no single record). Guard with `object&.…` if a condition is shared across action kinds. |
145
145
  | `params`, `request` | Current request. |
146
146
  | `current_user`, `current_parent` | The signed-in user and (nested) parent. |
147
- | `resource_record!` | The shown record on the show page; raises on index/table — prefer `object`. |
147
+ | `resource_record!` | The shown record on the show page; raises on index/table: prefer `object`. |
148
148
  | `allowed_to?`, `policy_for`, other helpers | The usual view helpers. |
149
149
 
150
150
  `object` is evaluated **per row** in tables and grids, so per-record show/hide works there too.
151
151
 
152
- ::: danger `condition:` is NOT authorization — it only hides the button
152
+ ::: danger `condition:` is NOT authorization: it only hides the button
153
153
  A hidden action still has a **live route**: anyone who knows the URL can still trigger it. `condition:` decides whether the *button renders*, never whether the *request is allowed*.
154
154
 
155
155
  ```ruby
156
- # 🚫 WRONG — this does NOT stop non-admins. The route is live; they can POST to it.
156
+ # 🚫 WRONG: this does NOT stop non-admins. The route is live; they can POST to it.
157
157
  action :wipe, interaction: WipeInteraction, condition: -> { current_user.admin? }
158
158
 
159
- # ✅ RIGHT — authorization belongs in the policy. The action only runs if this returns true.
159
+ # ✅ RIGHT: authorization belongs in the policy. The action only runs if this returns true.
160
160
  class WidgetPolicy < ResourcePolicy
161
161
  def wipe? = current_user.admin?
162
162
  end
163
163
  ```
164
164
 
165
- **Rule of thumb:** "who may run this" → **policy** (`def action_name?`). "is this UI relevant right now" → `condition:`. Authorization is enforced regardless of `condition:`; the two compose — an action appears only when the policy permits **and** the condition is truthy.
165
+ **Rule of thumb:** "who may run this" → **policy** (`def action_name?`). "is this UI relevant right now" → `condition:`. Authorization is enforced regardless of `condition:`; the two compose: an action appears only when the policy permits **and** the condition is truthy.
166
166
  :::
167
167
 
168
168
  ::: tip Per-record display vs. per-record authorization
169
- `condition: -> { object.draft? }` is fine for **showing/hiding** a per-record button. But if the rule is about **who may run it** ("only while draft *and* nobody else has it locked"), put it in the policy — `def publish? = record.draft?` is also evaluated per record (per row), and unlike `condition:` it actually gates execution.
169
+ `condition: -> { object.draft? }` is fine for **showing/hiding** a per-record button. But if the rule is about **who may run it** ("only while draft *and* nobody else has it locked"), put it in the policy: `def publish? = record.draft?` is also evaluated per record (per row), and unlike `condition:` it actually gates execution.
170
170
  :::
171
171
 
172
- ## Hidden actions — `hidden: true` {#hidden-actions}
172
+ ## Hidden actions: `hidden: true` {#hidden-actions}
173
173
 
174
- An action declared `hidden: true` renders in **no** toolbar, row dropdown, card, or bulk bar — regardless of its visibility flags, the policy, or `condition:`. Everything else about it stays live:
174
+ An action declared `hidden: true` renders in **no** toolbar, row dropdown, card, or bulk bar, regardless of its visibility flags, the policy, or `condition:`. Everything else about it stays live:
175
175
 
176
176
  - the **route** is mounted;
177
177
  - the **policy predicate** (`def name?`) is defined and enforced;
@@ -181,7 +181,7 @@ An action declared `hidden: true` renders in **no** toolbar, row dropdown, card,
181
181
  action :reposition, hidden: true
182
182
  ```
183
183
 
184
- The use case is an endpoint reached by **something other than a button** — a drag gesture, a custom Stimulus controller, a client-side widget you wrote yourself. The framework uses it for exactly that: [`position_on`](/reference/positioning) expands to `action :reposition, hidden: true`, and the kanban board's drop endpoint is declared the same way.
184
+ The use case is an endpoint reached by **something other than a button**: a drag gesture, a custom Stimulus controller, a client-side widget you wrote yourself. The framework uses it for exactly that: [`position_on`](/reference/resource/positioning) expands to `action :reposition, hidden: true`, and the kanban board's drop endpoint is declared the same way.
185
185
 
186
186
  ### `hidden:` vs `condition: -> { false }`
187
187
 
@@ -190,7 +190,7 @@ Both suppress the button, so pick by intent:
190
190
  | | `hidden: true` | `condition: -> { false }` |
191
191
  |---|---|---|
192
192
  | Decided | at **class-load**, once | at **render time**, per row/request |
193
- | Costs | nothing — the surfaces filter it out before any policy or condition runs | a proc evaluation per rendering |
193
+ | Costs | nothing: the surfaces filter it out before any policy or condition runs | a proc evaluation per rendering |
194
194
  | Says | "this is never a button" | "this is a button, just not right now" |
195
195
 
196
196
  Use `hidden:` for an action that is *structurally* not a button. Use `condition:` when visibility genuinely depends on the record, the user, or the request.
@@ -199,10 +199,10 @@ Use `hidden:` for an action that is *structurally* not a button. Use `condition:
199
199
  This is the same trap as [`condition:`](#conditional-visibility), and it bears repeating because "hidden" reads more absolute than it is. A hidden action has a **live route**: anyone who can construct the URL can call it.
200
200
 
201
201
  ```ruby
202
- # 🚫 WRONG — hiding the button does not stop the request.
202
+ # 🚫 WRONG: hiding the button does not stop the request.
203
203
  action :purge_all, interaction: PurgeInteraction, hidden: true
204
204
 
205
- # ✅ RIGHT — authorization belongs in the policy.
205
+ # ✅ RIGHT: authorization belongs in the policy.
206
206
  class WidgetPolicy < ResourcePolicy
207
207
  def purge_all? = current_user.admin?
208
208
  end
@@ -239,16 +239,16 @@ resources :posts do
239
239
  end
240
240
  ```
241
241
 
242
- Without it, `resource_url_for` can't build the URL — particularly critical for nested resources.
242
+ Without it, `resource_url_for` can't build the URL, particularly critical for nested resources.
243
243
  :::
244
244
 
245
245
  For anything with business logic, use an **interactive action** instead.
246
246
 
247
247
  ## Interactive actions
248
248
 
249
- Run an [Interaction](/reference/behavior/interactions) — automatically renders a form if the interaction declares attributes beyond `:resource`/`:resources`, otherwise executes immediately with a confirmation.
249
+ Run an [Interaction](/reference/behavior/interactions): automatically renders a form if the interaction declares attributes beyond `:resource`/`:resources`, otherwise executes immediately with a confirmation.
250
250
 
251
- The interactions below call named model methods (`archive!`, `invite!`) rather than doing the work inline. That's not mandatory for a one-off — the trigger to extract is the second caller, since an interaction can only be built with a `view_context`. See [Interactions › What an interaction is for](/reference/behavior/interactions#what-an-interaction-is-for).
251
+ The interactions below call named model methods (`archive!`, `invite!`) rather than doing the work inline. That's not mandatory for a one-off; the trigger to extract is the second caller, since an interaction can only be built with a `view_context`. See [Interactions › What an interaction is for](/reference/behavior/interactions#what-an-interaction-is-for).
252
252
 
253
253
  ```ruby
254
254
  class PostDefinition < Plutonium::Resource::Definition
@@ -341,7 +341,7 @@ action :bulk_archive, interaction: BulkArchiveInteraction
341
341
  # bulk_action: true inferred from `attribute :resources`
342
342
  ```
343
343
 
344
- Policy — checked per record; fails the whole request if ANY record is unauthorized:
344
+ Policy: checked per record; fails the whole request if ANY record is unauthorized:
345
345
 
346
346
  ```ruby
347
347
  def bulk_archive?
@@ -349,7 +349,7 @@ def bulk_archive?
349
349
  end
350
350
  ```
351
351
 
352
- The UI only shows bulk actions that ALL selected records support. Records are fetched via `current_authorized_scope` — users can only select records they can access.
352
+ The UI only shows bulk actions that ALL selected records support. Records are fetched via `current_authorized_scope`; users can only select records they can access.
353
353
 
354
354
  ### Resource action (no record)
355
355
 
@@ -375,7 +375,7 @@ action :import, interaction: ImportInteraction
375
375
 
376
376
  ## Running the work in the background
377
377
 
378
- An interactive action executes inside the request. When that is too slow — a bulk action over thousands of records, or a single call to something slow — replace `execute` with `async` and the interaction dispatches a persisted, resumable run instead:
378
+ An interactive action executes inside the request. When that is too slow (a bulk action over thousands of records, or a single call to something slow), replace `execute` with `async` and the interaction dispatches a persisted, resumable run instead:
379
379
 
380
380
  ```ruby
381
381
  class BulkArchiveInteraction < ResourceInteraction
@@ -396,8 +396,8 @@ See [Async Interactions](/reference/behavior/async-interactions) for failure pol
396
396
 
397
397
  | Interaction shape | Behavior |
398
398
  |---|---|
399
- | Only `:resource` / `:resources` (no extra inputs) | **Immediate** — browser confirmation (`"#{label}?"`, e.g. `"Archive?"`), then runs. Override with `confirmation: "Custom"` or `confirmation: false`. |
400
- | Additional `attribute` / `input` declared | **Form** — renders the action's form in a modal first; no auto-confirmation (the form is the confirmation). |
399
+ | Only `:resource` / `:resources` (no extra inputs) | **Immediate**: browser confirmation (`"#{label}?"`, e.g. `"Archive?"`), then runs. Override with `confirmation: "Custom"` or `confirmation: false`. |
400
+ | Additional `attribute` / `input` declared | **Form**: renders the action's form in a modal first; no auto-confirmation (the form is the confirmation). |
401
401
 
402
402
  ## Built-in CRUD actions
403
403
 
@@ -454,13 +454,13 @@ end
454
454
  ```
455
455
 
456
456
  > **CSV export is not an action.** It's a built-in, policy-gated capability with its own
457
- > button — see [CSV Export](./export.md). Don't declare it with `action :export_csv`.
457
+ > button; see [CSV Export](./export.md). Don't declare it with `action :export_csv`.
458
458
 
459
459
  ## Interaction responses
460
460
 
461
461
  ```ruby
462
462
  def execute
463
- # Success — redirects to resource automatically
463
+ # Success: redirects to resource automatically
464
464
  succeed(resource).with_message("Done!")
465
465
 
466
466
  # Different redirect destination
@@ -569,7 +569,7 @@ action :export,
569
569
 
570
570
  ## Related
571
571
 
572
- - [Definition](./definition) — fields, page chrome
573
- - [Query](./query) — search, filters, scopes
574
- - [Behavior › Interactions](/reference/behavior/interactions) — writing interaction classes
575
- - [Behavior › Policy](/reference/behavior/policies) — authorizing custom actions
572
+ - [Definition](./definition): fields, page chrome
573
+ - [Query](./query): search, filters, scopes
574
+ - [Behavior › Interactions](/reference/behavior/interactions): writing interaction classes
575
+ - [Behavior › Policy](/reference/behavior/policies): authorizing custom actions