plutonium 0.63.0 → 0.65.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 (280) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium/SKILL.md +4 -1
  3. data/.claude/skills/plutonium-app/SKILL.md +14 -0
  4. data/.claude/skills/plutonium-dashboard/SKILL.md +161 -0
  5. data/.claude/skills/plutonium-resource/SKILL.md +94 -14
  6. data/.claude/skills/plutonium-ui/SKILL.md +22 -1
  7. data/.standard.yml +1 -1
  8. data/CHANGELOG.md +65 -0
  9. data/CLAUDE.md +282 -210
  10. data/SECURITY.md +1 -1
  11. data/app/assets/plutonium-charts.js +20604 -0
  12. data/app/assets/plutonium-charts.js.map +7 -0
  13. data/app/assets/plutonium-charts.min.js +41 -0
  14. data/app/assets/plutonium-charts.min.js.map +7 -0
  15. data/app/assets/plutonium.css +1 -1
  16. data/app/assets/plutonium.js +794 -629
  17. data/app/assets/plutonium.js.map +4 -4
  18. data/app/assets/plutonium.min.js +48 -48
  19. data/app/assets/plutonium.min.js.map +4 -4
  20. data/app/views/plutonium/_flash_alerts.html.erb +2 -2
  21. data/app/views/plutonium/_resource_header.html.erb +2 -2
  22. data/app/views/plutonium/_resource_sidebar.html.erb +11 -2
  23. data/app/views/plutonium/_toast.html.erb +2 -2
  24. data/app/views/rodauth/_login_form.html.erb +1 -1
  25. data/app/views/rodauth/_password_visibility.html.erb +1 -1
  26. data/app/views/rodauth/add_recovery_codes.html.erb +2 -2
  27. data/app/views/rodauth/change_login.html.erb +2 -2
  28. data/app/views/rodauth/create_account.html.erb +4 -4
  29. data/app/views/rodauth/reset_password_request.html.erb +1 -1
  30. data/app/views/rodauth/verify_account_resend.html.erb +1 -1
  31. data/app/views/rodauth/webauthn_remove.html.erb +1 -1
  32. data/config/brakeman.ignore +4 -4
  33. data/config/initializers/rabl.rb +0 -40
  34. data/config/locales/en/api_client.yml +22 -0
  35. data/config/locales/en/async.yml +34 -0
  36. data/config/locales/en/dashboard.yml +25 -0
  37. data/config/locales/en/invites.yml +37 -0
  38. data/config/locales/en/js.yml +40 -0
  39. data/config/locales/en/kanban.yml +28 -0
  40. data/config/locales/en/plutonium.yml +26 -0
  41. data/config/locales/en/positioning.yml +14 -0
  42. data/config/locales/en/profile.yml +29 -0
  43. data/config/locales/en/resource.yml +60 -0
  44. data/config/locales/en/ui.yml +93 -0
  45. data/config/locales/en/wizard.yml +46 -0
  46. data/docs/.vitepress/config.ts +34 -1
  47. data/docs/.vitepress/theme/components/HomeHero.vue +1 -1
  48. data/docs/.vitepress/theme/components/HomeInTheBox.vue +14 -0
  49. data/docs/.vitepress/theme/custom.css +34 -377
  50. data/docs/blog/introducing-plutonium-dashboards.md +175 -0
  51. data/docs/blog/introducing-plutonium-i18n.md +91 -0
  52. data/docs/blog/introducing-plutonium.md +4 -5
  53. data/docs/blog/whats-new-async-kanban-wizards.md +1 -1
  54. data/docs/guides/dashboards.md +300 -0
  55. data/docs/guides/index.md +1 -0
  56. data/docs/guides/kanban.md +2 -2
  57. data/docs/public/images/blog/dashboards-card-error.png +0 -0
  58. data/docs/public/images/blog/dashboards-loading.png +0 -0
  59. data/docs/public/images/blog/dashboards-overview.png +0 -0
  60. data/docs/public/images/blog/dashboards-sidebar.png +0 -0
  61. data/docs/public/images/blog/i18n-es-admin.png +0 -0
  62. data/docs/public/images/guides/dashboard-card-error.png +0 -0
  63. data/docs/public/images/guides/dashboard-loading.png +0 -0
  64. data/docs/public/images/guides/dashboard-overview.png +0 -0
  65. data/docs/public/images/guides/dashboard-sidebar.png +0 -0
  66. data/docs/reference/app/generators.md +2 -2
  67. data/docs/reference/app/portals.md +2 -0
  68. data/docs/reference/configuration.md +84 -27
  69. data/docs/reference/dashboard/dsl.md +123 -0
  70. data/docs/reference/dashboard/index.md +35 -0
  71. data/docs/reference/dashboard/registration.md +76 -0
  72. data/docs/reference/i18n.md +239 -0
  73. data/docs/reference/index.md +10 -1
  74. data/docs/reference/kanban/dsl.md +1 -1
  75. data/docs/reference/kanban/positioning.md +2 -2
  76. data/docs/reference/resource/actions.md +1 -1
  77. data/docs/reference/resource/definition.md +96 -18
  78. data/docs/reference/resource/export.md +5 -1
  79. data/docs/reference/resource/index.md +2 -0
  80. data/docs/reference/{positioning.md → resource/positioning.md} +2 -1
  81. data/docs/reference/resource/query.md +2 -0
  82. data/docs/reference/ui/assets.md +8 -0
  83. data/docs/reference/ui/displays.md +5 -8
  84. data/docs/reference/ui/index.md +1 -1
  85. data/docs/reference/wizard/dsl.md +2 -2
  86. data/esbuild.config.js +5 -2
  87. data/gemfiles/rails_7.gemfile.lock +3 -3
  88. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  89. data/gemfiles/rails_8.1.gemfile.lock +2 -2
  90. data/lib/active_model/validations/array_validator.rb +1 -1
  91. data/lib/active_model/validations/attached_validator.rb +1 -1
  92. data/lib/active_model/validations/url_validator.rb +2 -2
  93. data/lib/generators/pu/core/assets/assets_generator.rb +36 -21
  94. data/lib/generators/pu/core/assets/templates/postcss.config.js +1 -1
  95. data/lib/generators/pu/core/update/update_generator.rb +3 -2
  96. data/lib/generators/pu/dashboard/dashboard_generator.rb +96 -0
  97. data/lib/generators/pu/dashboard/templates/dashboard.rb.tt +25 -0
  98. data/lib/generators/pu/docker/install/install_generator.rb +58 -0
  99. data/lib/generators/pu/docker/install/templates/Dockerfile.dev.tt +18 -3
  100. data/lib/generators/pu/docker/install/templates/Dockerfile.tt +20 -5
  101. data/lib/generators/pu/docker/install/templates/{docker-compose.yml → docker-compose.yml.tt} +1 -1
  102. data/lib/generators/pu/gem/dotenv/dotenv_generator.rb +1 -1
  103. data/lib/generators/pu/invites/invitable_generator.rb +18 -1
  104. data/lib/generators/pu/lib/plutonium_generators/concerns/actions.rb +0 -95
  105. data/lib/generators/pu/lib/plutonium_generators/concerns/js_package_manager.rb +146 -0
  106. data/lib/generators/pu/lib/plutonium_generators/generator.rb +1 -17
  107. data/lib/generators/pu/pkg/package/package_generator.rb +1 -0
  108. data/lib/generators/pu/pkg/package/templates/config/locales/en.yml.tt +28 -0
  109. data/lib/generators/pu/pkg/portal/portal_generator.rb +1 -0
  110. data/lib/generators/pu/pkg/portal/templates/config/locales/en.yml.tt +30 -0
  111. data/lib/generators/pu/res/scaffold/scaffold_generator.rb +0 -8
  112. data/lib/generators/pu/rodauth/concerns/feature_selector.rb +0 -18
  113. data/lib/plutonium/action/base.rb +27 -3
  114. data/lib/plutonium/action/interactive.rb +17 -4
  115. data/lib/plutonium/api_client/concerns/create_api_client.rb +12 -11
  116. data/lib/plutonium/api_client/concerns/disable_api_client.rb +3 -3
  117. data/lib/plutonium/auth/sequel_adapter.rb +1 -1
  118. data/lib/plutonium/configuration.rb +5 -0
  119. data/lib/plutonium/core/controller.rb +25 -1
  120. data/lib/plutonium/core/controllers/authorizable.rb +1 -1
  121. data/lib/plutonium/dashboard/base.rb +114 -0
  122. data/lib/plutonium/dashboard/card.rb +200 -0
  123. data/lib/plutonium/dashboard/controller.rb +101 -0
  124. data/lib/plutonium/dashboard/dsl.rb +104 -0
  125. data/lib/plutonium/dashboard/register.rb +40 -0
  126. data/lib/plutonium/dashboard/route_resolution.rb +30 -0
  127. data/lib/plutonium/dashboard.rb +42 -0
  128. data/lib/plutonium/definition/actions.rb +6 -1
  129. data/lib/plutonium/definition/base.rb +41 -3
  130. data/lib/plutonium/definition/index_views.rb +1 -1
  131. data/lib/plutonium/definition/positioning.rb +1 -1
  132. data/lib/plutonium/definition/presentable.rb +2 -2
  133. data/lib/plutonium/helpers/assets_helper.rb +15 -2
  134. data/lib/plutonium/helpers/display_helper.rb +10 -2
  135. data/lib/plutonium/interaction/async/context.rb +6 -8
  136. data/lib/plutonium/interaction/async/executor.rb +6 -7
  137. data/lib/plutonium/interaction/async/run.rb +2 -2
  138. data/lib/plutonium/interaction/async/run_definition.rb +1 -1
  139. data/lib/plutonium/interaction/base.rb +2 -0
  140. data/lib/plutonium/interaction/concerns/dispatchable.rb +15 -3
  141. data/lib/plutonium/invites/concerns/cancel_invite.rb +3 -3
  142. data/lib/plutonium/invites/concerns/invite_token.rb +25 -7
  143. data/lib/plutonium/invites/concerns/invite_user.rb +10 -5
  144. data/lib/plutonium/invites/concerns/resend_invite.rb +4 -4
  145. data/lib/plutonium/invites/controller.rb +11 -11
  146. data/lib/plutonium/kanban/column.rb +13 -3
  147. data/lib/plutonium/kanban/dsl.rb +7 -4
  148. data/lib/plutonium/models/has_cents.rb +1 -1
  149. data/lib/plutonium/profile/security_section.rb +10 -18
  150. data/lib/plutonium/query/filter.rb +10 -1
  151. data/lib/plutonium/query/filters/association.rb +27 -2
  152. data/lib/plutonium/query/filters/boolean.rb +4 -4
  153. data/lib/plutonium/query/filters/date.rb +4 -17
  154. data/lib/plutonium/query/filters/date_range.rb +2 -2
  155. data/lib/plutonium/query/filters/select.rb +10 -1
  156. data/lib/plutonium/query/filters/text.rb +9 -12
  157. data/lib/plutonium/railtie.rb +17 -0
  158. data/lib/plutonium/resource/controller.rb +10 -5
  159. data/lib/plutonium/resource/controllers/crud_actions.rb +17 -20
  160. data/lib/plutonium/resource/controllers/export_csv.rb +19 -9
  161. data/lib/plutonium/resource/controllers/kanban_actions.rb +16 -16
  162. data/lib/plutonium/resource/controllers/position_actions.rb +5 -5
  163. data/lib/plutonium/resource/controllers/typeahead.rb +24 -1
  164. data/lib/plutonium/resource/query_object.rb +30 -3
  165. data/lib/plutonium/routing/dashboard_registration.rb +137 -0
  166. data/lib/plutonium/routing/resource_registration.rb +5 -0
  167. data/lib/plutonium/routing/route_set_extensions.rb +2 -1
  168. data/lib/plutonium/translation.rb +227 -0
  169. data/lib/plutonium/ui/actions_dropdown.rb +2 -2
  170. data/lib/plutonium/ui/breadcrumbs.rb +3 -3
  171. data/lib/plutonium/ui/color_mode_selector.rb +1 -1
  172. data/lib/plutonium/ui/component/behaviour.rb +46 -0
  173. data/lib/plutonium/ui/component/methods.rb +4 -0
  174. data/lib/plutonium/ui/dashboard/board.rb +80 -0
  175. data/lib/plutonium/ui/dashboard/card.rb +127 -0
  176. data/lib/plutonium/ui/dashboard/chart.rb +43 -0
  177. data/lib/plutonium/ui/dashboard/custom.rb +32 -0
  178. data/lib/plutonium/ui/dashboard/frame.rb +25 -0
  179. data/lib/plutonium/ui/dashboard/metric.rb +137 -0
  180. data/lib/plutonium/ui/dashboard/skeleton.rb +40 -0
  181. data/lib/plutonium/ui/display/components/attachment.rb +1 -1
  182. data/lib/plutonium/ui/display/components/badge.rb +7 -1
  183. data/lib/plutonium/ui/display/components/boolean.rb +2 -2
  184. data/lib/plutonium/ui/display/resource.rb +23 -16
  185. data/lib/plutonium/ui/export_button.rb +3 -3
  186. data/lib/plutonium/ui/form/components/key_value_store.rb +5 -5
  187. data/lib/plutonium/ui/form/components/password.rb +2 -2
  188. data/lib/plutonium/ui/form/components/resource_select.rb +27 -0
  189. data/lib/plutonium/ui/form/components/secure_association.rb +2 -0
  190. data/lib/plutonium/ui/form/components/uppy.rb +11 -3
  191. data/lib/plutonium/ui/form/concerns/renders_nested_resource_fields.rb +1 -1
  192. data/lib/plutonium/ui/form/concerns/renders_repeater_row_controls.rb +5 -4
  193. data/lib/plutonium/ui/form/concerns/renders_structured_inputs.rb +1 -1
  194. data/lib/plutonium/ui/form/query.rb +8 -8
  195. data/lib/plutonium/ui/form/resource.rb +54 -26
  196. data/lib/plutonium/ui/frame_navigator_panel.rb +4 -4
  197. data/lib/plutonium/ui/grid/card.rb +3 -3
  198. data/lib/plutonium/ui/grid/resource.rb +3 -3
  199. data/lib/plutonium/ui/interaction/async/run_progress.rb +6 -6
  200. data/lib/plutonium/ui/interaction/async/running_banner.rb +2 -2
  201. data/lib/plutonium/ui/kanban/column.rb +6 -6
  202. data/lib/plutonium/ui/layout/base.rb +19 -1
  203. data/lib/plutonium/ui/layout/header.rb +1 -1
  204. data/lib/plutonium/ui/layout/icon_rail.rb +4 -4
  205. data/lib/plutonium/ui/layout/rodauth_layout.rb +1 -1
  206. data/lib/plutonium/ui/layout/sidebar.rb +1 -1
  207. data/lib/plutonium/ui/layout/topbar.rb +2 -2
  208. data/lib/plutonium/ui/modal/base.rb +4 -4
  209. data/lib/plutonium/ui/nav_grid_menu.rb +1 -1
  210. data/lib/plutonium/ui/nav_user.rb +2 -2
  211. data/lib/plutonium/ui/page/base.rb +10 -2
  212. data/lib/plutonium/ui/page/dashboard.rb +54 -0
  213. data/lib/plutonium/ui/page/edit.rb +3 -3
  214. data/lib/plutonium/ui/page/interactive_action.rb +2 -2
  215. data/lib/plutonium/ui/page/new.rb +3 -3
  216. data/lib/plutonium/ui/page/show.rb +2 -2
  217. data/lib/plutonium/ui/page/wizard.rb +22 -16
  218. data/lib/plutonium/ui/page/wizard_chooser.rb +10 -10
  219. data/lib/plutonium/ui/page/wizard_completed.rb +2 -2
  220. data/lib/plutonium/ui/skeleton_table.rb +1 -1
  221. data/lib/plutonium/ui/table/base.rb +2 -2
  222. data/lib/plutonium/ui/table/components/attachment.rb +1 -1
  223. data/lib/plutonium/ui/table/components/bulk_actions_toolbar.rb +19 -3
  224. data/lib/plutonium/ui/table/components/drag_handle.rb +3 -3
  225. data/lib/plutonium/ui/table/components/filter_form.rb +11 -10
  226. data/lib/plutonium/ui/table/components/filter_pills.rb +4 -4
  227. data/lib/plutonium/ui/table/components/pagy_info.rb +61 -17
  228. data/lib/plutonium/ui/table/components/pagy_pagination.rb +9 -5
  229. data/lib/plutonium/ui/table/components/row_actions_dropdown.rb +1 -1
  230. data/lib/plutonium/ui/table/components/scopes_bar.rb +6 -2
  231. data/lib/plutonium/ui/table/components/scopes_pills.rb +3 -3
  232. data/lib/plutonium/ui/table/components/selection_column.rb +1 -1
  233. data/lib/plutonium/ui/table/components/toolbar.rb +2 -2
  234. data/lib/plutonium/ui/table/components/view_switcher.rb +12 -8
  235. data/lib/plutonium/ui/table/resource.rb +53 -17
  236. data/lib/plutonium/ui/wizard/review.rb +7 -7
  237. data/lib/plutonium/ui/wizard/stepper.rb +1 -1
  238. data/lib/plutonium/ui/wizard/summary_display.rb +6 -0
  239. data/lib/plutonium/version.rb +1 -1
  240. data/lib/plutonium/wizard/base.rb +2 -0
  241. data/lib/plutonium/wizard/driving.rb +8 -2
  242. data/lib/plutonium/wizard/dsl.rb +4 -2
  243. data/lib/plutonium/wizard/field_importer.rb +1 -8
  244. data/lib/plutonium/wizard/review_step.rb +8 -1
  245. data/lib/plutonium/wizard/step.rb +14 -3
  246. data/package.json +4 -2
  247. data/plutonium.gemspec +15 -30
  248. data/postcss-gem-import.cjs +34 -0
  249. data/postcss-gem-import.js +3 -28
  250. data/src/css/components.css +91 -0
  251. data/src/css/slim_select.css +0 -38
  252. data/src/css/tokens.css +24 -0
  253. data/src/js/controllers/attachment_input_controller.js +15 -6
  254. data/src/js/controllers/chart_controller.js +162 -0
  255. data/src/js/controllers/clipboard_controller.js +3 -2
  256. data/src/js/controllers/dirty_form_guard_controller.js +4 -3
  257. data/src/js/controllers/easymde_controller.js +3 -0
  258. data/src/js/controllers/filter_panel_controller.js +15 -6
  259. data/src/js/controllers/flatpickr_controller.js +8 -0
  260. data/src/js/controllers/frame_refresh_controller.js +59 -0
  261. data/src/js/controllers/intl_tel_input_controller.js +5 -0
  262. data/src/js/controllers/register_controllers.js +4 -0
  263. data/src/js/controllers/slim_select_controller.js +79 -176
  264. data/src/js/core.js +9 -1
  265. data/src/js/i18n.js +69 -0
  266. data/src/js/plutonium-charts.js +15 -0
  267. data/src/js/turbo/turbo_confirm.js +5 -3
  268. metadata +71 -31
  269. data/.yarnrc.yml +0 -8
  270. data/config/initializers/hotwire_turbo_monkey_patches.rb +0 -12
  271. data/lib/generators/pu/field/input/input_generator.rb +0 -32
  272. data/lib/generators/pu/field/input/templates/input.rb.tt +0 -15
  273. data/lib/generators/pu/lib/plutonium_generators/concerns/config.rb +0 -39
  274. data/lib/generators/pu/lib/plutonium_generators/concerns/serializer.rb +0 -39
  275. data/lib/generators/pu/lib/plutonium_generators/installer.rb +0 -205
  276. data/lib/generators/pu/res/scaffold/templates/presenter.rb.tt +0 -13
  277. data/lib/generators/pu/res/scaffold/templates/query_object.rb.tt +0 -17
  278. data/public/plutonium-assets/application.js +0 -31419
  279. data/public/plutonium-assets/plutonium.ico +0 -0
  280. data/yarn.lock +0 -6858
@@ -12,9 +12,11 @@ aside: false
12
12
  :rail="[
13
13
  { group: 'App', items: [
14
14
  { name: 'Overview', link: '/plutonium-core/reference/app/' },
15
+ { name: 'Configuration', link: '/plutonium-core/reference/configuration' },
15
16
  { name: 'Packages', link: '/plutonium-core/reference/app/packages' },
16
17
  { name: 'Portals', link: '/plutonium-core/reference/app/portals' },
17
18
  { name: 'Generators', link: '/plutonium-core/reference/app/generators' },
19
+ { name: 'Lite (SQLite) generators', link: '/plutonium-core/reference/generators/lite' },
18
20
  ]},
19
21
  { group: 'Resource', items: [
20
22
  { name: 'Overview', link: '/plutonium-core/reference/resource/' },
@@ -22,13 +24,15 @@ aside: false
22
24
  { name: 'Definition', link: '/plutonium-core/reference/resource/definition' },
23
25
  { name: 'Query', link: '/plutonium-core/reference/resource/query' },
24
26
  { name: 'Actions', link: '/plutonium-core/reference/resource/actions' },
25
- { name: 'Positioning', link: '/plutonium-core/reference/positioning' },
27
+ { name: 'Positioning', link: '/plutonium-core/reference/resource/positioning' },
28
+ { name: 'CSV Export', link: '/plutonium-core/reference/resource/export' },
26
29
  ]},
27
30
  { group: 'Behavior', items: [
28
31
  { name: 'Overview', link: '/plutonium-core/reference/behavior/' },
29
32
  { name: 'Controllers', link: '/plutonium-core/reference/behavior/controllers' },
30
33
  { name: 'Policies', link: '/plutonium-core/reference/behavior/policies' },
31
34
  { name: 'Interactions', link: '/plutonium-core/reference/behavior/interactions' },
35
+ { name: 'Async interactions', link: '/plutonium-core/reference/behavior/async-interactions' },
32
36
  ]},
33
37
  { group: 'UI', items: [
34
38
  { name: 'Overview', link: '/plutonium-core/reference/ui/' },
@@ -65,6 +69,11 @@ aside: false
65
69
  { name: 'Positioning', link: '/plutonium-core/reference/kanban/positioning' },
66
70
  { name: 'Authorization', link: '/plutonium-core/reference/kanban/authorization' },
67
71
  ]},
72
+ { group: 'Dashboard', items: [
73
+ { name: 'Overview', link: '/plutonium-core/reference/dashboard/' },
74
+ { name: 'DSL', link: '/plutonium-core/reference/dashboard/dsl' },
75
+ { name: 'Registration', link: '/plutonium-core/reference/dashboard/registration' },
76
+ ]},
68
77
  { group: 'Testing', items: [
69
78
  { name: 'Overview', link: '/plutonium-core/reference/testing/' },
70
79
  ]},
@@ -49,7 +49,7 @@ 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
54
  #### Mode A — delegate to `Plutonium::Positioning::Model` (default)
55
55
 
@@ -1,7 +1,7 @@
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
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)).
@@ -152,7 +152,7 @@ end
152
152
 
153
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.
154
154
 
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.
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/resource/positioning#mode-b) for a worked `acts_as_list` example.
156
156
 
157
157
  ### Mode C — disabled
158
158
 
@@ -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
 
@@ -159,6 +159,8 @@ input :title,
159
159
  wrapper: {class: "col-span-full"}
160
160
  ```
161
161
 
162
+ Label, hint, placeholder and description need no declaration to be translatable: leave them out and Plutonium looks them up under `activerecord.attributes` and `plutonium.fields.<model>.<attr>` respectively. An explicit value can be a translation too, via the class-level `t`. See [Internationalization](../i18n#fields-placeholder-hint-description).
163
+
162
164
  ## Select / choices
163
165
 
164
166
  ### Static
@@ -196,6 +198,13 @@ display :rejection_reason, condition: -> { object.rejected? }
196
198
  field :debug_info, condition: -> { Rails.env.development? }
197
199
  ```
198
200
 
201
+ A surface's own `condition:` wins. Otherwise the form, the show page and the table
202
+ each fall back to the `field` condition, so it runs everywhere. The table has no
203
+ `object`, which means a `field` condition must not reference the record:
204
+ `-> { object.published? }` works on the form and show page and raises on the index.
205
+ Keep a `field` condition to context that exists on every surface (`Rails.env`, a
206
+ feature flag, `current_user`) and put record checks on `input`, `display` or `column`.
207
+
199
208
  ::: warning UI state, not authorization
200
209
  `condition:` is for UI logic ("show this when published"). For "who can see this", use the policy's `permitted_attributes_for_*` — see [Behavior › Policy](/reference/behavior/policies).
201
210
  :::
@@ -293,24 +302,41 @@ input :birth_date do |f|
293
302
  end
294
303
  ```
295
304
 
296
- ### `phlexi_render` for declarative custom display
305
+ ### Inline markup in a display
297
306
 
298
- `as: :phlexi_render` (or its shorthand `as: :phlexi`). `with:` takes either a Phlex component class OR a proc whose body is **rendered inside a Phlex context** — HTML tag methods (`span`, `div`, `a`) and Tailwind classes are first-class. The proc receives `(value, attrs)`.
307
+ A `display` block is evaluated in the display component's Phlex context, so HTML tag
308
+ methods (`span`, `div`, `a`) and Tailwind classes are first-class, and `field.object`
309
+ is the record. It renders once, even for a `has_many` field.
299
310
 
300
311
  ```ruby
301
- # Component — preferred for anything reusable
302
- display :status, as: :phlexi_render, with: StatusBadgeComponent
312
+ display :priority do |field|
313
+ variant = {"high" => "danger", "medium" => "warning"}.fetch(field.value.to_s, "info")
314
+ span(class: "pu-badge pu-badge-#{variant}") { field.value.to_s.humanize }
315
+ end
303
316
 
304
- # Inline proc — `span` here is a Phlex tag method, not a Rails helper
305
- display :priority, as: :phlexi_render, with: ->(value, attrs) {
306
- case value
307
- when 'high' then span(class: "badge badge-danger") { "High" }
308
- when 'medium' then span(class: "badge badge-warning") { "Medium" }
309
- else span(class: "badge badge-info") { "Low" }
317
+ # Reaches the record; renders once for a collection
318
+ display :tags do |field|
319
+ div(class: "flex flex-wrap gap-1.5") do
320
+ field.object.tags.each { |tag| span(class: "pu-badge") { tag.name } }
310
321
  end
311
- }
322
+ end
312
323
  ```
313
324
 
325
+ Phlex also renders whatever the block returns, so a block that emits markup must end
326
+ with a tag call (or `nil`). The `div` wrapper above does that. A bare
327
+ `tags.each { span … }` as the last line returns the collection, which renders a
328
+ second time as text.
329
+
330
+ ::: warning `as: :phlexi_render` is deprecated
331
+ `display :x, as: :phlexi_render, with: ...` emits a deprecation warning and will be
332
+ removed. Use the block form above instead. The old `with:` proc did not run in a
333
+ Phlex context (its `self` was the definition, so `span`/`div` in the body raised), it
334
+ received the value already stringified so it could not reach the record, and it fired
335
+ once per element for a `has_many` field. The block form has none of those limits. This
336
+ deprecation applies only to the `as: :phlexi_render` display option; the internal
337
+ `phlexi_render` helper is unaffected.
338
+ :::
339
+
314
340
  See [UI › Components](/reference/ui/components) for writing reusable Phlex components.
315
341
 
316
342
  ### Custom component class
@@ -338,27 +364,65 @@ See [UI › Components › Field components](/reference/ui/components#field-comp
338
364
 
339
365
  ## Column options
340
366
 
367
+ `align:` and `label:` configure the column header. They can be declared on `field`,
368
+ `display` or `column`; the most specific wins.
369
+
341
370
  ```ruby
371
+ field :amount, align: :end # every surface that has a notion of alignment
342
372
  column :title, align: :start # default
343
373
  column :status, align: :center
344
- column :amount, align: :end
374
+ column :amount, label: "Total"
375
+ ```
376
+
377
+ ### What a column inherits from the display
378
+
379
+ With no `column` declared, the table column renders exactly as the display does: the
380
+ display's `as:` and its attributes flow to the cell, so `display :status, as: :badge,
381
+ colors: {…}` colors the badge on the show page and in the table.
382
+
383
+ A `column` that only touches the header (`align:`, `label:`, `condition:`) keeps that
384
+ inheritance. A `column` that says anything about rendering (an `as:`, a component
385
+ attribute, or a block) renders alone: its own `as:` and attributes, nothing from the
386
+ display.
387
+
388
+ A `condition:` on `field` or `column` hides the column. A `condition:` on `display`
389
+ applies to the show page only, because it may reference the record and the table has
390
+ no single one.
391
+
392
+ ```ruby
393
+ display :status, as: :badge, colors: {published: :accent}
394
+ column :status, align: :center # still a badge, still colored
395
+ column :status, as: :badge # a badge, but with default colors
396
+ column :status, formatter: ->(v) { v.upcase } # plain text; the display is out
345
397
  ```
346
398
 
347
399
  ### Value formatting
348
400
 
349
401
  `formatter:` receives just the value. Use a block when you need the full record.
402
+ A column block runs in the table page's Phlex context, the same way a display block
403
+ runs in the show page's: it can emit markup directly, or return a String, a number
404
+ or a component. `self` is the table page, so `current_user`, `helpers` and
405
+ `resource_definition` are available, but methods defined on the definition class are
406
+ not; reach through the record you are passed instead.
350
407
 
351
408
  ```ruby
352
409
  column :description, formatter: ->(value) { value&.truncate(30) }
353
410
  column :price, formatter: ->(value) { "$%.2f" % value if value }
354
- column :status, formatter: ->(value) { value&.humanize&.upcase }
355
411
 
356
- # Block — full record access
412
+ # Block with full record access: emit markup...
413
+ column :status do |record|
414
+ span(class: "pu-badge") { record.status.humanize }
415
+ end
416
+
417
+ # ...or return a String
357
418
  column :full_name do |record|
358
419
  "#{record.first_name} #{record.last_name}"
359
420
  end
360
421
  ```
361
422
 
423
+ As with display blocks, a block that emits markup must end with a tag call (or
424
+ `nil`), because Phlex renders the return value too.
425
+
362
426
  ## Nested inputs
363
427
 
364
428
  Inline forms for associated records. Requires `accepts_nested_attributes_for` on the model.
@@ -580,11 +644,25 @@ Section options previously took a **zero-argument** proc evaluated against the f
580
644
  It fails loudly, never silently. `condition:` is unchanged — it is still evaluated against the form and still reads `object` with no argument.
581
645
  :::
582
646
 
583
- A section that resolves to **zero fields** — every declared field filtered out by the permitted set, or no field assigned — renders nothing at all (no heading, no grid). This keeps forms clean when fewer attributes are permitted than declared (notably `+ New`, where the create policy often permits a subset). The check is purely "are there fields to render"; it does **not** evaluate per-field `condition:` procs (those run later, at field render). So if you want a whole section to appear only under some state, gate it with the section's own `condition:` rather than relying on every field inside it being hidden:
647
+ A section that resolves to **zero fields** — every declared field filtered out by the permitted set, or no field assigned — renders nothing at all (no heading, no grid). This keeps forms clean when fewer attributes are permitted than declared (notably `+ New`, where the create policy often permits a subset).
648
+
649
+ The same goes for a section whose fields are **all hidden by their own `condition:`** on this render: it disappears with them instead of leaving an empty heading behind. The hidden fields are still recorded on the form, just as a hidden field in a visible section is. So fields that only apply to some records — re-evaluated on `pre_submit` — can be grouped in a section without any extra wiring:
650
+
651
+ ```ruby
652
+ form_layout do
653
+ section :timing, :duration, :starts_on, label: "Timing" # gone when both are hidden
654
+ end
655
+
656
+ input :employment_type, as: :select, pre_submit: true, choices: %w[full_time contract]
657
+ input :duration, condition: -> { object.contract? }
658
+ input :starts_on, condition: -> { object.contract? }
659
+ ```
660
+
661
+ A section's own `condition:` still hides it as a unit, whatever its fields say:
584
662
 
585
663
  ```ruby
586
664
  section :shipping, :address, :city, :postcode,
587
- condition: -> { object.requires_shipping? } # hide the section as a unit
665
+ condition: -> { object.requires_shipping? }
588
666
  ```
589
667
 
590
668
  ### `ungrouped(**opts)`
@@ -664,7 +742,7 @@ end
664
742
 
665
743
  ## Display layout
666
744
 
667
- The show page's counterpart to [`form_layout`](#form-layout). Same DSL and the same resolution rules — first-section-wins ownership, unlisted permitted fields collected into `ungrouped`, absent fields skipped, zero-field sections dropped entirely — applied to the show page instead of the form.
745
+ The show page's counterpart to [`form_layout`](#form-layout). Same DSL and the same resolution rules — first-section-wins ownership, unlisted permitted fields collected into `ungrouped`, absent fields skipped, zero-field sections and sections whose fields are all condition-hidden dropped entirely — applied to the show page instead of the form.
668
746
 
669
747
  ```ruby
670
748
  class PostDefinition < ResourceDefinition
@@ -770,7 +848,7 @@ All three inherit to subclasses, so a portal-specific definition keeps its paren
770
848
 
771
849
  - **Modals are unaffected** — a dialog sizes itself via `modal_size`.
772
850
  - **Interactions** (`Plutonium::Interaction::Base`) support the same settings, for interactive actions rendered as standalone pages.
773
- - **Wizards are configured separately**, via `Plutonium.configuration.wizards.width`. It defaults to `default_page_width`, so a wizard step carrying a `form_layout` renders those sections at the same width the identical sections get on a resource form.
851
+ - **Wizards are configured separately**, via `Plutonium.configuration.wizards.width`. It defaults to `:md` independently of `default_page_width`, so widening resource pages leaves wizard steps where they are. Set both if you want them to match.
774
852
 
775
853
  ## File uploads
776
854
 
@@ -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+).
@@ -42,6 +42,8 @@ A `field :title` with no options that matches what Plutonium would auto-detect i
42
42
  - [Definition](./definition) — fields, inputs, displays, columns, page chrome, metadata panel, index views
43
43
  - [Query](./query) — search, filters, scopes, sorting
44
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
 
@@ -469,7 +469,8 @@ The client moves the row **optimistically**, so the response is deliberately qui
469
469
  |---|---|
470
470
  | Clean Mode A drop, both neighbours resolved | `204 No Content` — nothing repaints |
471
471
  | `reposition!` had to rebalance the group | `200` + turbo-stream of the collection |
472
- | A neighbour did not resolve, or belongs to another positioning group | `200` + stream |
472
+ | A neighbour did not resolve, or belongs to another positioning group, but the other one anchored the drop | `200` + stream — the record **did** move, so there is nothing to explain |
473
+ | Both neighbours rejected | `200` + stream, **no write** — plus a toast when the cause was a foreign group rather than transient drift |
473
474
  | Mode B (opaque block write) | `200` + stream |
474
475
  | `reposition?` denied | `403` + stream + toast (the row snaps back) |
475
476
  | `index?` denied | `403`, **no body** — you may not see the list, so the refusal must not carry it to you |
@@ -84,6 +84,8 @@ For large tables, write an explicit `search` block backed by a trigram or full-t
84
84
 
85
85
  Six built-in filter types. Use the shorthand symbol or the full class name.
86
86
 
87
+ `search`, `scope`, `sort_fields`, and `sort_directions` are reserved filter names — they're built-in controls in the `q[<name>]` namespace, so `filter :scope` raises `ArgumentError`.
88
+
87
89
  | Type | Symbol | Params in URL | Options |
88
90
  |---|---|---|---|
89
91
  | Text | `:text` | `query` | `predicate:` |
@@ -38,6 +38,14 @@ This:
38
38
  4. Registers Plutonium's Stimulus controllers.
39
39
  5. Updates Plutonium config to point at your asset files.
40
40
 
41
+ ### Package managers
42
+
43
+ The generator installs with the package manager `rails javascript:build` will use. The lockfile decides: `bun.lock` or `bun.lockb` means bun, `pnpm-lock.yaml` pnpm, `package-lock.json` npm, `yarn.lock` yarn. With no lockfile, `bun.config.js` means bun, and otherwise the first of bun, yarn, pnpm or npm found on PATH wins (yarn if none is). That is why `rails new -j esbuild` on a machine with bun installed produces a bun app. When the app's jsbundling-rails provides `Jsbundling::PackageManager` (its `main` branch, not yet in a release as of 1.3.1), the generator defers to it instead; that detector ignores `yarn.lock` and picks by PATH. Every package goes through one `add` with versions inline (`tailwindcss@latest`, `@radioactive-labs/plutonium@^<gem version>`), which yarn 1, yarn 2+, bun, npm and pnpm all understand.
44
+
45
+ Yarn 2+ apps get `nodeLinker: node-modules` written to `.yarnrc.yml` if no linker is set. Tailwind's PostCSS plugin does not load under Plug'n'Play.
46
+
47
+ `pu:core:update` and `pu:docker:install` use the same detection, so the Dockerfile installs bun, yarn 1, or yarn 2+ via corepack to match the app.
48
+
41
49
  ## Tailwind config
42
50
 
43
51
  Generated `tailwind.config.js`:
@@ -53,14 +53,11 @@ class PostDefinition < ResourceDefinition
53
53
  StatusBadgeComponent.new(value: field.value, class: field.dom.css_class)
54
54
  end
55
55
 
56
- # phlexi_render — proc whose body is rendered inside a Phlex context
57
- display :priority, as: :phlexi_render, with: ->(value, attrs) {
58
- case value
59
- when 'high' then span(class: "badge badge-danger") { "High" }
60
- when 'medium' then span(class: "badge badge-warning") { "Medium" }
61
- else span(class: "badge badge-info") { "Low" }
62
- end
63
- }
56
+ # Block that emits markup directly (evaluated in the display's Phlex context)
57
+ display :priority do |field|
58
+ variant = {"high" => "danger", "medium" => "warning"}.fetch(field.value.to_s, "info")
59
+ span(class: "pu-badge pu-badge-#{variant}") { field.value.to_s.humanize }
60
+ end
64
61
 
65
62
  # Field component class — built as ChartComponent.new(field, **attributes),
66
63
  # so it subclasses Phlexi::Display::Components::Base and reads `field`.
@@ -6,7 +6,7 @@ Plutonium uses [Phlex](https://www.phlex.fun/) for all view components and Tailw
6
6
 
7
7
  - [Pages](./pages) — `IndexPage`, `ShowPage`, `NewPage`, `EditPage`, render hooks, custom ERB views, context detection
8
8
  - [Forms](./forms) — `Form` class, field builder, association inputs (typeahead + inline add), themes
9
- - [Displays](./displays) — `Display` class, custom rendering, `phlexi_render`
9
+ - [Displays](./displays) — `Display` class, custom rendering, block-form displays
10
10
  - [Tables](./tables) — `Table` class, custom rendering, search/scopes bar
11
11
  - [Components](./components) — built-in component kit, custom Phlex components, `DynaFrameContent` pattern, modals & tabs
12
12
  - [Layouts](./layouts) — shell config, ejecting chrome, custom `ResourceLayout` class
@@ -30,7 +30,7 @@ For task-oriented walkthroughs, start with the [Wizards guide](/guides/wizards).
30
30
  | `one_time` | Retain the completed row at the `concurrency_key` (blocks restart, gate-able). **Requires a `concurrency_key`.** Omit → row deleted on completion (repeatable). See [One-time wizards](/reference/wizard/one-time). |
31
31
  | `completed do \|wizard\| … end` | Custom body for the "already completed" page a finished **one-time** wizard shows when re-opened (replaces the default confirmation). See [`completed`](#completed) below and [One-time wizards](/reference/wizard/one-time#re-opening-a-completed-wizard). |
32
32
  | `encrypt_data` | Encrypt the staged `data` column at rest using ActiveRecord's encryption keys (off by default), for flows that stage PII. Requires `active_record.encryption` keys — see [Storage & config](/reference/wizard/storage-config#encryption). |
33
- | `width <size>` | Width of this wizard's step pages: `:sm` `:md` `:lg` `:xl` `:full`. Unset inherits `config.wizards.width` (default `:md`). Deliberately independent of `config.default_page_width` — see [Storage & config](/reference/wizard/storage-config). |
33
+ | `width <size>` | Width of this wizard's step pages: `:sm` `:md` `:lg` `:xl` `:full`. Inherits to subclasses; otherwise `config.wizards.width` (default `:md`). Deliberately independent of `config.default_page_width` — see [Storage & config](/reference/wizard/storage-config). |
34
34
  | `anonymous` | Opt into **guest (unauthenticated) access** — the wizard runs pre-login (auth is required otherwise). The guest's identity is a server-minted run-id in the Rails session; it crosses the auth boundary only at its terminal `execute`. Mount it `public: true` (the default for `anonymous`). **Mutually exclusive with `concurrency_key`/`one_time`** — a guest is already session-keyed and repeatable, so declaring both raises (whichever is declared last). See [Authentication](/reference/wizard/anchoring-resume#authentication). |
35
35
 
36
36
  ```ruby
@@ -235,7 +235,7 @@ What it renders depends on completion state and the `summary:` / block options:
235
235
  | `summary:` | Show the auto-summary of completed steps (default `true`). When `false`, the complete-state body is your block — or the built-in "ready to complete" panel if there's no block. The summary always renders in the incomplete state. |
236
236
  | `header:` | Show the step-header section (the label plus the "check everything over" prompt, which only appears when the summary is shown) above the body (default `true`). `false` drops it for a chromeless finish. |
237
237
 
238
- The auto-summary renders each field through the display pipeline, honoring the input's declared `as:`/`label:`:
238
+ The auto-summary renders each field through the display pipeline, honoring the input's declared `as:`/`label:`. An input hidden by its own `condition:` is left out, just as it was left off the step, so the summary never shows an empty row for a question the user wasn't asked:
239
239
 
240
240
  - a **choice input** (`select`/`radio_buttons` with `choices:`) resolves the stored value back to its label (`"pro"` → `"Pro"`) using the same choice mapper the form uses, so the recap matches what the user picked;
241
241
  - an **`as: :currency`** input formats the value as currency (`"1500.5"` → `"$1,500.50"`) rather than echoing a bare decimal — pass `unit:` on the input (the data snapshot has no `has_cents` reflection to infer it from).
data/esbuild.config.js CHANGED
@@ -6,7 +6,8 @@ if (process.argv.includes("--dev")) {
6
6
  _context({
7
7
  outdir: "src/build",
8
8
  entryPoints: [
9
- "src/js/plutonium.js"
9
+ "src/js/plutonium.js",
10
+ "src/js/plutonium-charts.js"
10
11
  ],
11
12
  plugins: [
12
13
  manifestPlugin({
@@ -24,6 +25,7 @@ else {
24
25
  outdir: "src/dist/js",
25
26
  entryPoints: [
26
27
  "src/js/plutonium.js",
28
+ "src/js/plutonium-charts.js"
27
29
  ],
28
30
  minify,
29
31
  sourcemap: true,
@@ -34,7 +36,8 @@ else {
34
36
  _build({
35
37
  outdir: "app/assets",
36
38
  entryPoints: [
37
- "src/js/plutonium.js"
39
+ "src/js/plutonium.js",
40
+ "src/js/plutonium-charts.js"
38
41
  ],
39
42
  minify,
40
43
  sourcemap: true,
@@ -1,11 +1,11 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- plutonium (0.62.2)
4
+ plutonium (0.64.0)
5
5
  action_policy (~> 0.7.0)
6
6
  csv
7
7
  listen (~> 3.8)
8
- pagy (~> 43.0)
8
+ pagy (~> 43.0, >= 43.5.6)
9
9
  phlex (~> 2.0)
10
10
  phlex-rails
11
11
  phlex-slotable (>= 1.0.0)
@@ -601,7 +601,7 @@ CHECKSUMS
601
601
  phlexi-form (0.14.3) sha256=47d69c4a49ba22279ba541b9b110e81d62acc4602513f21bb5c3d29fa9c8d5ca
602
602
  phlexi-menu (0.4.1) sha256=f6eeb1eb9597caadeee33a53f1520b3613a6ac616cf1e395e9d734a412895fb2
603
603
  phlexi-table (0.2.0) sha256=73bee135adbe73fcdb6fdd2672444373556dd9e6bba61e7b5b20ca8b7a1f822c
604
- plutonium (0.62.2)
604
+ plutonium (0.64.0)
605
605
  pp (0.6.4) sha256=dfcb0fce700c41456265922884f9fe195d7fbb0674a3578e6c0f69588e82b570
606
606
  prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193
607
607
  prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
@@ -5,7 +5,7 @@ PATH
5
5
  action_policy (~> 0.7.0)
6
6
  csv
7
7
  listen (~> 3.8)
8
- pagy (~> 43.0)
8
+ pagy (~> 43.0, >= 43.5.6)
9
9
  phlex (~> 2.0)
10
10
  phlex-rails
11
11
  phlex-slotable (>= 1.0.0)
@@ -1,11 +1,11 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- plutonium (0.62.2)
4
+ plutonium (0.64.0)
5
5
  action_policy (~> 0.7.0)
6
6
  csv
7
7
  listen (~> 3.8)
8
- pagy (~> 43.0)
8
+ pagy (~> 43.0, >= 43.5.6)
9
9
  phlex (~> 2.0)
10
10
  phlex-rails
11
11
  phlex-slotable (>= 1.0.0)
@@ -52,7 +52,7 @@ module ActiveModel
52
52
  errors = record.errors.delete attribute
53
53
  return if errors.nil?
54
54
 
55
- @existing_errors += errors.map { |e| "item #{index + 1} #{e}" }
55
+ @existing_errors += errors.map { |e| I18n.t("errors.messages.array_item", index: index + 1, message: e) }
56
56
  end
57
57
 
58
58
  def skip?(value, validator_options)
@@ -10,7 +10,7 @@ module ActiveModel
10
10
  #
11
11
  class AttachedValidator < EachValidator
12
12
  def validate_each(record, attribute, value)
13
- record.errors.add(attribute, options[:message] || "must be attached") unless value.attached?
13
+ record.errors.add(attribute, options[:message] || :not_attached) unless value.attached?
14
14
  end
15
15
  end
16
16
  end
@@ -35,7 +35,7 @@ module ActiveModel
35
35
  def validate_url
36
36
  return true if URL_PATTERN.match?(value)
37
37
 
38
- record.errors.add attribute, (options[:message] || "is not a valid URL")
38
+ record.errors.add attribute, (options[:message] || :invalid_url)
39
39
  false
40
40
  end
41
41
 
@@ -43,7 +43,7 @@ module ActiveModel
43
43
  return true unless options[:image].present?
44
44
  return true unless FastImage.type(value).nil?
45
45
 
46
- record.errors.add(attribute, options[:message] || "is not a valid image URL")
46
+ record.errors.add(attribute, options[:message] || :invalid_image_url)
47
47
  false
48
48
  end
49
49
  end
@@ -25,15 +25,23 @@ module Pu
25
25
 
26
26
  private
27
27
 
28
- # The asset pipeline assumes the app was generated with esbuild + Tailwind.
29
- # Without those, `application.tailwind.css` doesn't exist and the generator
30
- # later crashes with a cryptic inject_into_file error. Fail early with a fix.
28
+ # The asset pipeline assumes the app was generated with esbuild + Tailwind + Stimulus.
29
+ # Without those, `application.tailwind.css` (Tailwind) and/or
30
+ # `app/javascript/controllers/index.js` (Stimulus) don't exist, and the generator
31
+ # later crashes with a cryptic inject_into_file error. Fail early listing every
32
+ # missing file with a fix.
31
33
  def verify_prerequisites
32
- return if File.exist?("app/assets/stylesheets/application.tailwind.css")
34
+ required = [
35
+ "app/assets/stylesheets/application.tailwind.css",
36
+ "app/javascript/controllers/index.js"
37
+ ]
38
+ missing = required.reject { |path| File.exist?(path) }
39
+ return if missing.empty?
33
40
 
34
41
  error <<~MSG
35
- Plutonium assets require a Rails app generated with esbuild and Tailwind.
36
- Expected app/assets/stylesheets/application.tailwind.css, but it is missing.
42
+ Plutonium assets require a Rails app generated with esbuild, Tailwind, and Stimulus.
43
+ Missing files:
44
+ - #{missing.join("\n - ")}
37
45
 
38
46
  Re-create the app with the required flags:
39
47
  rails new myapp -a propshaft -j esbuild -c tailwind \\
@@ -46,28 +54,35 @@ module Pu
46
54
  copy_file "postcss.config.js", force: true
47
55
  end
48
56
 
57
+ # Every package goes through one `add` built for the app's package manager
58
+ # (bun, yarn 1, yarn 2+, npm or pnpm; see Concerns::JsPackageManager).
59
+ # Versions ride inline because `pkg@latest` and `pkg@^x.y.z` are the one
60
+ # spelling all of them share; yarn 1's `upgrade --latest` does not exist
61
+ # on yarn 2+, and running yarn inside a bun app leaves two lockfiles.
49
62
  def install_dependencies
50
- failed = []
63
+ ensure_yarn_node_modules_linker
51
64
 
65
+ command = js_add_command(npm_packages)
66
+ return if run(command)
67
+
68
+ say_status :warn,
69
+ "`#{command}` failed — your app may not boot until you re-run it",
70
+ :yellow
71
+ end
72
+
73
+ def npm_packages
52
74
  [
53
- "@radioactive-labs/plutonium",
75
+ # Pinned to the gem so the two halves never drift; pu:core:update
76
+ # keeps them together afterwards.
77
+ "@radioactive-labs/plutonium@^#{Plutonium::VERSION}",
78
+ # cssbundling-rails already installs the latest Tailwind on new apps;
79
+ # apps created on Tailwind 3 are moved to 4 here.
80
+ "tailwindcss@latest",
54
81
  "postcss", "postcss-cli", "postcss-import",
55
82
  "@tailwindcss/postcss", "@tailwindcss/forms", "@tailwindcss/typography",
56
83
  "cssnano", "marked",
57
84
  "flowbite-typography"
58
- ].each do |package|
59
- run "yarn add #{package}"
60
- failed << "yarn add #{package}" unless $?.success?
61
- end
62
-
63
- run "yarn upgrade tailwindcss --latest"
64
- failed << "yarn upgrade tailwindcss --latest" unless $?.success?
65
-
66
- return if failed.empty?
67
-
68
- say_status :warn,
69
- "the following commands failed — your app may not boot until you resolve them:\n #{failed.join("\n ")}",
70
- :yellow
85
+ ]
71
86
  end
72
87
 
73
88
  def configure_application
@@ -3,7 +3,7 @@ const plutoniumGemPath = execSync("bundle show plutonium").toString().trim();
3
3
 
4
4
  module.exports = {
5
5
  plugins: {
6
- [`${plutoniumGemPath}/postcss-gem-import.js`]: {},
6
+ [`${plutoniumGemPath}/postcss-gem-import.cjs`]: {},
7
7
  'postcss-import': {},
8
8
  '@tailwindcss/postcss': {},
9
9
  cssnano: {}
@@ -45,9 +45,10 @@ module Pu
45
45
 
46
46
  def update_npm_package
47
47
  gem_version = installed_gem_version
48
+ package = gem_version ? "@radioactive-labs/plutonium@^#{gem_version}" : "@radioactive-labs/plutonium@latest"
48
49
 
49
- say_status :update, "Updating @radioactive-labs/plutonium to #{gem_version}...", :green
50
- run "yarn add @radioactive-labs/plutonium@^#{gem_version}"
50
+ say_status :update, "Updating #{package} with #{js_package_manager}...", :green
51
+ run js_add_command(package)
51
52
  end
52
53
 
53
54
  def installed_gem_version