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
@@ -88,10 +88,10 @@ module Plutonium
88
88
  ) do
89
89
  div(class: "px-6 pt-5 pb-4 border-b border-[var(--pu-border)]") do
90
90
  h2(id: "pu-dirty-guard-title", class: "text-lg font-semibold text-[var(--pu-text)]") do
91
- "Discard changes?"
91
+ t("plutonium.ui.form.discard_title")
92
92
  end
93
93
  p(id: "pu-dirty-guard-desc", class: "mt-1 text-sm text-[var(--pu-text-muted)]") do
94
- "You have unsaved changes. Closing this form now will lose them."
94
+ t("plutonium.ui.form.discard_description")
95
95
  end
96
96
  end
97
97
  div(class: "flex items-center justify-end gap-2 px-6 py-4") do
@@ -99,12 +99,12 @@ module Plutonium
99
99
  type: "button",
100
100
  class: "pu-btn pu-btn-md pu-btn-outline",
101
101
  data: {action: "dirty-form-guard#keepEditing"}
102
- ) { "Keep editing" }
102
+ ) { t("plutonium.ui.form.keep_editing") }
103
103
  button(
104
104
  type: "button",
105
105
  class: "pu-btn pu-btn-md pu-btn-danger",
106
106
  data: {action: "dirty-form-guard#discard"}
107
- ) { "Discard changes" }
107
+ ) { t("plutonium.ui.form.discard_changes") }
108
108
  end
109
109
  end
110
110
  end
@@ -125,6 +125,12 @@ module Plutonium
125
125
  div(class: themed(:sections_wrapper, nil)) do
126
126
  sections.each { |rs| render_form_section(rs) }
127
127
  end
128
+ # Fields of sections dropped because every one of them is
129
+ # condition-hidden. They still go through the normal render path,
130
+ # which records them without emitting markup (see
131
+ # render_simple_resource_field), so the form keeps extracting their
132
+ # values exactly as it does for a hidden field in a visible section.
133
+ @condition_hidden_section_fields.each { |name| render_resource_field name }
128
134
  end
129
135
  end
130
136
 
@@ -134,6 +140,7 @@ module Plutonium
134
140
  # helpers live — same context as input/section `condition:`). Returns nil
135
141
  # when no form_layout is declared (caller falls back to a single grid).
136
142
  def resolve_form_layout
143
+ @condition_hidden_section_fields = []
137
144
  sections = resource_definition.resolve_form_sections(resource_fields)
138
145
  return nil if sections.nil?
139
146
 
@@ -146,12 +153,19 @@ module Plutonium
146
153
  # filtered out by the permitted set (policy, per-action, scoping,
147
154
  # nesting). Rendering the chrome (heading + empty grid) for these
148
155
  # litters the form with empty headings (notably on `+ New`, where
149
- # fewer attributes are permitted than declared). Field-level
150
- # `condition:` is evaluated later, at render — a section whose fields
151
- # are all condition-hidden is the author's call to gate via the
152
- # section's own `condition:`.
156
+ # fewer attributes are permitted than declared).
153
157
  next if resolved.fields.empty?
154
158
 
159
+ # Likewise drop sections whose fields are ALL hidden by their own
160
+ # `condition:` on this render (e.g. a "Timing" section of fields that
161
+ # only apply to some record types, re-evaluated on `pre_submit`).
162
+ # Their fields are kept aside and rendered after the sections, so
163
+ # the hidden values are still recorded.
164
+ if resolved.fields.all? { |name| field_condition_hidden?(name) }
165
+ @condition_hidden_section_fields.concat(resolved.fields)
166
+ next
167
+ end
168
+
155
169
  # `columns` stays a validated literal (the Builder rejects a non-Integer
156
170
  # at declaration); `label`/`description`/`collapsible`/`collapsed` may be
157
171
  # procs, resolved by the SAME arity rule as a field/input option so
@@ -268,7 +282,7 @@ module Plutonium
268
282
  end
269
283
 
270
284
  def render_submit_and_continue_button
271
- label = object.new_record? ? "Create and add another" : "Update and continue editing"
285
+ label = object.new_record? ? t("plutonium.ui.form.create_and_add_another") : t("plutonium.ui.form.update_and_continue_editing")
272
286
 
273
287
  button(
274
288
  type: :submit,
@@ -315,19 +329,22 @@ module Plutonium
315
329
 
316
330
  tag = input_options[:as] || field_options[:as]
317
331
 
318
- # Extract field-level options from input_options and merge into field_options
319
- # These are Phlexi field options that should be passed to form.field(), not to the tag builder
320
- # Note: forms use :hint, displays use :description
321
- field_level_keys = [:hint, :label, :placeholder]
322
- field_level_options = input_options.slice(*field_level_keys)
332
+ # Route the input's own field-level options to form.field(); strip the
333
+ # full union from the tag attributes so a display-only key (e.g.
334
+ # :description) declared here is dropped rather than leaked as an HTML
335
+ # attribute.
336
+ field_level_options = input_options.slice(*FORM_FIELD_LEVEL_KEYS)
323
337
  field_options = field_options.merge(field_level_options)
338
+ # Slots the definition left blank fall back to the locale convention
339
+ # (plutonium.fields.<model>.<attr>.placeholder / .hint).
340
+ field_options = Plutonium::Translation.fill_field_text(field_options, form.object.class, name, :placeholder, :hint)
324
341
 
325
342
  # Staging options are stripped here as well as in Wizard::StepAdapter,
326
343
  # because an interaction's inputs reach this method directly. A
327
344
  # Class-valued `uploader:` is not an HTML attribute and Phlex refuses
328
345
  # it, so leaving it in raises rather than rendering.
329
346
  tag_attributes = input_options.except(
330
- :wrapper, :as, :pre_submit, :condition, *field_level_keys,
347
+ :wrapper, :as, :pre_submit, :condition, *FIELD_LEVEL_KEYS,
331
348
  *Plutonium::Attachments::STAGING_ONLY_INPUT_OPTIONS
332
349
  )
333
350
  if input_options[:pre_submit]
@@ -344,10 +361,15 @@ module Plutonium
344
361
  # renders correctly. The Builder consumes `:as` before Phlexi sees
345
362
  # the options, so this cannot leak into a Phlexi field option.
346
363
  field_options = field_options.merge(as: tag) if tag
347
- field_options = field_options.except(:condition)
348
364
 
365
+ # Check for conditional rendering — read `:condition` from
366
+ # `field_options` BEFORE stripping it, so a `field :name,
367
+ # condition: -> {...}` declaration is honoured, not just
368
+ # `input :name, condition: -> {...}`. Mirrors the display
369
+ # renderer, which reads then removes (see display/resource.rb).
349
370
  condition = input_options[:condition] || field_options[:condition]
350
371
  conditionally_hidden = condition && !instance_exec(&condition)
372
+ field_options = field_options.except(:condition)
351
373
  if conditionally_hidden
352
374
  # Do not render the field, but still create field
353
375
  # Phlexi form will record it without rendering it, allowing us to extract its value
@@ -411,18 +433,24 @@ module Plutonium
411
433
  # is why it cannot take a `form` argument the way an option does.
412
434
  # Resolving it here would also collapse it to a boolean before the render
413
435
  # site that owns it gets to ask.
414
- def resolve_option_procs(options)
415
- return options if options.blank?
416
-
417
- options.to_h do |key, value|
418
- [key, resolvable_proc?(key, value) ? call_option_proc(value) : value]
419
- end
436
+ # Whether +name+ is hidden on this render by its own `condition:` — the
437
+ # same check render_resource_field makes before rendering it: a nested
438
+ # input's condition, else the `input`/`field` condition. Structured
439
+ # inputs have no condition of their own and always count as visible.
440
+ def field_condition_hidden?(name)
441
+ definition = resource_definition
442
+ condition =
443
+ if definition.respond_to?(:defined_nested_inputs) && (nested = definition.defined_nested_inputs[name])
444
+ nested[:options]&.fetch(:condition, nil)
445
+ elsif definition.respond_to?(:defined_structured_inputs) && definition.defined_structured_inputs[name]
446
+ nil
447
+ else
448
+ definition.defined_inputs.dig(name, :options, :condition) ||
449
+ definition.defined_fields.dig(name, :options, :condition)
450
+ end
451
+ condition.present? && !instance_exec(&condition)
420
452
  end
421
453
 
422
- def resolvable_proc?(key, value) = key != :condition && value.is_a?(Proc)
423
-
424
- def call_option_proc(value) = value.arity.zero? ? value.call : value.call(self)
425
-
426
454
  def when_permitted(name, &)
427
455
  return unless resource_fields.include? name
428
456
 
@@ -66,10 +66,10 @@ module Plutonium
66
66
  div(data: {controller: %w[frame-navigator]}) do
67
67
  Panel do |panel|
68
68
  panel.with_title @title
69
- panel.with_item PanelItem.new(label: "Home", icon: Phlex::TablerIcons::Home2, data_frame_navigator_target: "homeButton")
70
- panel.with_item PanelItem.new(label: "Back", icon: Phlex::TablerIcons::ChevronLeft, data_frame_navigator_target: "backButton")
71
- panel.with_item PanelItem.new(label: "Refresh", icon: Phlex::TablerIcons::RefreshDot, data_frame_navigator_target: "refreshButton")
72
- panel.with_item PanelLink.new(label: "Maximize", icon: Phlex::TablerIcons::WindowMaximize, href: @src, data_frame_navigator_target: "maximizeLink")
69
+ panel.with_item PanelItem.new(label: t("plutonium.ui.home"), icon: Phlex::TablerIcons::Home2, data_frame_navigator_target: "homeButton")
70
+ panel.with_item PanelItem.new(label: t("plutonium.ui.frame_navigator.back"), icon: Phlex::TablerIcons::ChevronLeft, data_frame_navigator_target: "backButton")
71
+ panel.with_item PanelItem.new(label: t("plutonium.ui.frame_navigator.refresh"), icon: Phlex::TablerIcons::RefreshDot, data_frame_navigator_target: "refreshButton")
72
+ panel.with_item PanelLink.new(label: t("plutonium.ui.frame_navigator.maximize"), icon: Phlex::TablerIcons::WindowMaximize, href: @src, data_frame_navigator_target: "maximizeLink")
73
73
  panel.with_content PanelContent.new(id: @panel_id, src: @src)
74
74
  end
75
75
  end
@@ -200,7 +200,7 @@ module Plutonium
200
200
  label = helpers.display_name_of(value)
201
201
  variant = badge.variant_for(label)
202
202
  else
203
- label = badge.humanize(value)
203
+ label = Plutonium::Translation.value_label(@record.class, name, value) || badge.humanize(value)
204
204
  variant = badge.variant_for(value)
205
205
  end
206
206
 
@@ -269,8 +269,8 @@ module Plutonium
269
269
  data: {row_click_target: "show", turbo_frame: @show_turbo_frame || show.turbo_frame(resource_definition)},
270
270
  class: "sr-only",
271
271
  tabindex: "-1",
272
- "aria-label": "Open #{header_text}"
273
- })) { plain "Open" }
272
+ "aria-label": t("plutonium.ui.grid.open_record", name: header_text)
273
+ })) { plain t("plutonium.ui.grid.open") }
274
274
  end
275
275
 
276
276
  # ---------------------------------------------------------------
@@ -63,7 +63,7 @@ module Plutonium
63
63
  end
64
64
 
65
65
  def render_empty_card
66
- EmptyCard("No #{resource_name_plural(resource_class).downcase} available") {
66
+ EmptyCard(t("plutonium.ui.empty_state.none_available", resources: resource_name_plural(resource_class).downcase)) {
67
67
  action = resource_definition.defined_actions[:new]
68
68
  if action&.permitted_by?(current_policy)
69
69
  url = route_options_to_url(action.route_options, resource_class)
@@ -160,7 +160,7 @@ module Plutonium
160
160
  "data-[open]:translate-x-0 " \
161
161
  "flex flex-col",
162
162
  role: "dialog",
163
- aria: {label: "Filters", hidden: "true", modal: "true"},
163
+ aria: {label: t("plutonium.ui.table.filters"), hidden: "true", modal: "true"},
164
164
  data: {filter_panel_target: "panel"}
165
165
  ) do
166
166
  render Plutonium::UI::Table::Components::FilterForm.new(
@@ -181,7 +181,7 @@ module Plutonium
181
181
 
182
182
  def render_footer
183
183
  div(class: "lg:sticky bottom-[-2px] mt-1 p-4 pb-6 w-full z-30 bg-[var(--pu-body)]") {
184
- TableInfo(pagy_instance)
184
+ TableInfo(pagy_instance, item_name: resource_name(resource_class, pagy_instance.count))
185
185
  TablePagination(pagy_instance)
186
186
  }
187
187
  end
@@ -118,7 +118,7 @@ module Plutonium
118
118
  def render_status
119
119
  div(class: "flex items-center gap-2 flex-wrap") do
120
120
  span(class: "pu-badge pu-badge-#{BADGE_VARIANTS.fetch(run.outcome, "neutral")}") do
121
- plain run.outcome.humanize
121
+ plain(Plutonium::Translation.value_label(run.class, :outcome, run.outcome) || run.outcome.humanize)
122
122
  end
123
123
  # The count rides alongside the badge rather than only in the list
124
124
  # below: a partially-applied :continue run ends as "completed", and
@@ -126,7 +126,7 @@ module Plutonium
126
126
  # do everything it was asked to.
127
127
  if run.error_count.positive?
128
128
  span(class: "text-xs font-medium text-[var(--pu-text-danger)]") do
129
- plain "#{run.error_count} #{"error".pluralize(run.error_count)}"
129
+ plain t("plutonium.async.progress.error_count", count: run.error_count)
130
130
  end
131
131
  end
132
132
  end
@@ -151,7 +151,7 @@ module Plutonium
151
151
  div(class: "h-2 rounded-full bg-primary-600", style: "width: #{percent}%")
152
152
  end
153
153
  div(class: "text-xs text-[var(--pu-text-muted)]") do
154
- plain "#{run.progress_done} of #{run.progress_total} targets (#{percent}%)"
154
+ plain t("plutonium.async.progress.targets", done: run.progress_done, total: run.progress_total, percent: percent)
155
155
  end
156
156
  end
157
157
  end
@@ -175,7 +175,7 @@ module Plutonium
175
175
  end
176
176
  end
177
177
  div(class: "text-xs text-[var(--pu-text-muted)]") do
178
- plain(run.in_progress? ? "Working…" : "No progress total was recorded")
178
+ plain(run.in_progress? ? t("plutonium.async.progress.working") : t("plutonium.async.progress.no_total"))
179
179
  end
180
180
  end
181
181
  end
@@ -204,7 +204,7 @@ module Plutonium
204
204
  def render_failures
205
205
  div(class: "space-y-1") do
206
206
  div(class: "text-[10px] font-semibold uppercase tracking-wider text-[var(--pu-text-muted)]") do
207
- plain "Failures"
207
+ plain t("plutonium.async.progress.failures")
208
208
  end
209
209
  ul(class: "pu-run-progress-failures text-xs space-y-1") do
210
210
  run.errors_log.each do |entry|
@@ -218,7 +218,7 @@ module Plutonium
218
218
  # not a target whose id happens to be blank.
219
219
  def failure_text(entry)
220
220
  target_id = entry["target_id"]
221
- target_id.nil? ? entry["message"].to_s : "##{target_id}: #{entry["message"]}"
221
+ target_id.nil? ? entry["message"].to_s : t("plutonium.async.progress.target_failure", target_id: target_id, message: entry["message"])
222
222
  end
223
223
  end
224
224
  end
@@ -51,10 +51,10 @@ module Plutonium
51
51
  # own page renders the pair.
52
52
  span(class: "font-medium text-[var(--pu-text)]") { plain run.to_label }
53
53
  plain " "
54
- span(class: "text-[var(--pu-text-muted)]") { plain run.state.humanize }
54
+ span(class: "text-[var(--pu-text-muted)]") { plain(Plutonium::Translation.value_label(run.class, :state, run.state) || run.state.humanize) }
55
55
  end
56
56
  a(href: resource_url_for(run), class: "pu-btn pu-btn-xs pu-btn-soft-primary shrink-0") do
57
- plain "View progress"
57
+ plain t("plutonium.async.banner.view_progress")
58
58
  end
59
59
  end
60
60
  end
@@ -162,7 +162,7 @@ module Plutonium
162
162
  button(
163
163
  class: "p-0.5 rounded text-[var(--pu-text-muted)] " \
164
164
  "hover:text-[var(--pu-text)] hover:bg-[var(--pu-surface-alt)]",
165
- title: "Expand #{column.label}",
165
+ title: t("plutonium.kanban.column.expand", column: column.label),
166
166
  type: "button",
167
167
  data: {
168
168
  action: "click->kanban#toggleColumn",
@@ -214,7 +214,7 @@ module Plutonium
214
214
  button(
215
215
  class: "shrink-0 p-0.5 rounded text-[var(--pu-text-muted)] " \
216
216
  "hover:text-[var(--pu-text)] hover:bg-[var(--pu-surface-alt)]",
217
- title: "Collapse #{column.label}",
217
+ title: t("plutonium.kanban.column.collapse", column: column.label),
218
218
  type: "button",
219
219
  data: {
220
220
  action: "click->kanban#toggleColumn",
@@ -237,7 +237,7 @@ module Plutonium
237
237
  "pu-badge text-xs font-mono",
238
238
  over ? "pu-badge-danger" : "pu-badge-neutral"
239
239
  ),
240
- title: over ? "WIP limit exceeded" : "WIP limit: #{column.wip}"
240
+ title: over ? t("plutonium.kanban.column.wip_limit_exceeded") : t("plutonium.kanban.column.wip_limit", limit: column.wip)
241
241
  ) do
242
242
  plain "#{cards.size}/#{column.wip}"
243
243
  end
@@ -281,7 +281,7 @@ module Plutonium
281
281
  def render_more_footer
282
282
  div(class: "px-3 py-2 border-t border-[var(--pu-border)] bg-[var(--pu-surface)]") do
283
283
  span(class: "text-xs text-[var(--pu-text-muted)]") do
284
- plain "+#{more_count} more"
284
+ plain t("plutonium.kanban.column.more", count: more_count)
285
285
  end
286
286
  end
287
287
  end
@@ -296,10 +296,10 @@ module Plutonium
296
296
  link_to(
297
297
  @column_add_url,
298
298
  class: "pu-btn pu-btn-ghost pu-btn-xs text-[var(--pu-text-muted)]",
299
- title: "Add to #{column.label}",
299
+ title: t("plutonium.kanban.column.add_to", column: column.label),
300
300
  data: {turbo_frame: Plutonium::REMOTE_MODAL_FRAME}
301
301
  ) do
302
- plain "+ Add"
302
+ plain t("plutonium.kanban.column.add")
303
303
  end
304
304
  end
305
305
 
@@ -21,7 +21,7 @@ module Plutonium
21
21
 
22
22
  private
23
23
 
24
- def lang = nil
24
+ def lang = I18n.locale.to_s
25
25
 
26
26
  def page_title = view_context.controller.instance_variable_get(:@page_title)
27
27
 
@@ -35,6 +35,7 @@ module Plutonium
35
35
  head {
36
36
  render_title
37
37
  render_metatags
38
+ render_i18n
38
39
  render_assets
39
40
  render_pre_paint_scripts
40
41
  }
@@ -175,6 +176,23 @@ module Plutonium
175
176
  render_scripts
176
177
  end
177
178
 
179
+ # The `plutonium.js` locale subtree as JSON for src/js/i18n.js, in a
180
+ # <meta> rather than a <script>: Turbo Drive replaces provisional head
181
+ # elements on every visit but only ever appends scripts, so a locale
182
+ # switch mid-session would otherwise keep serving the first blob.
183
+ #
184
+ # Merged over the default locale so a partially translated locale
185
+ # falls back per key instead of dropping every untranslated string.
186
+ def render_i18n
187
+ meta(name: "pu-i18n", content: i18n_blob.to_json)
188
+ end
189
+
190
+ def i18n_blob
191
+ defaults = I18n.t("plutonium.js", locale: I18n.default_locale, default: {})
192
+ current = (I18n.locale == I18n.default_locale) ? {} : I18n.t("plutonium.js", default: {})
193
+ defaults.deep_merge(current)
194
+ end
195
+
178
196
  def render_styles
179
197
  render_external_styles
180
198
 
@@ -109,7 +109,7 @@ module Plutonium
109
109
  render Phlex::TablerIcons::X.new(class: "w-6 h-6")
110
110
  end
111
111
 
112
- span(class: "sr-only") { "Toggle sidebar" }
112
+ span(class: "sr-only") { t("plutonium.ui.layout.toggle_sidebar") }
113
113
  end
114
114
 
115
115
  # Renders the action buttons section
@@ -38,7 +38,7 @@ module Plutonium
38
38
  aside(
39
39
  id: "sidebar-navigation",
40
40
  data: {controller: "sidebar icon-rail"},
41
- aria: {label: "Sidebar Navigation"},
41
+ aria: {label: t("plutonium.ui.layout.sidebar_navigation")},
42
42
  class: "fixed top-0 left-0 z-40 h-dvh " \
43
43
  "bg-[var(--pu-surface)] border-r border-[var(--pu-border)] " \
44
44
  "flex flex-col transition-[width] duration-200 overflow-x-hidden " \
@@ -56,7 +56,7 @@ module Plutonium
56
56
  div(class: "h-12 flex items-center justify-center border-b border-[var(--pu-border)] shrink-0") do
57
57
  next unless brand_slot?
58
58
 
59
- a(href: root_path, aria: {label: "Home"}, class: "flex items-center justify-center") do
59
+ a(href: root_path, aria: {label: t("plutonium.ui.home")}, class: "flex items-center justify-center") do
60
60
  render brand_slot
61
61
  end
62
62
  end
@@ -81,8 +81,8 @@ module Plutonium
81
81
  def render_pin_button
82
82
  button(
83
83
  type: "button",
84
- title: "Toggle sidebar",
85
- aria: {label: "Toggle sidebar pin"},
84
+ title: t("plutonium.ui.layout.toggle_sidebar"),
85
+ aria: {label: t("plutonium.ui.layout.toggle_sidebar_pin")},
86
86
  data: {action: "icon-rail#togglePin"},
87
87
  class: "flex items-center justify-center w-10 h-10 rounded-md transition-colors " \
88
88
  "text-[var(--pu-text-muted)] hover:text-[var(--pu-text)] hover:bg-[var(--pu-surface-alt)]"
@@ -45,7 +45,7 @@ module Plutonium
45
45
  def render_links
46
46
  link_to root_path, class: "mt-4 inline-flex items-center gap-1.5 font-medium text-secondary-600 dark:text-secondary-400 hover:underline transition-colors" do
47
47
  render Phlex::TablerIcons::Home2.new(class: "size-5")
48
- plain "Home"
48
+ plain t("plutonium.ui.home")
49
49
  end
50
50
  end
51
51
  end
@@ -17,7 +17,7 @@ module Plutonium
17
17
  aside(
18
18
  data: {controller: "sidebar"},
19
19
  id: "sidebar-navigation",
20
- aria: {label: "Sidebar Navigation"},
20
+ aria: {label: t("plutonium.ui.layout.sidebar_navigation")},
21
21
  class: "fixed top-0 left-0 z-40 w-64 h-dvh pt-14 transition-transform -translate-x-full lg:translate-x-0"
22
22
  ) do
23
23
  div(
@@ -55,7 +55,7 @@ module Plutonium
55
55
  type: "button",
56
56
  data_action: "resource-header#toggleDrawer",
57
57
  aria_controls: "#sidebar-navigation",
58
- aria_label: "Toggle sidebar",
58
+ aria_label: t("plutonium.ui.layout.toggle_sidebar"),
59
59
  class: "p-1.5 -ml-1.5 text-[var(--pu-text-muted)] rounded-md " \
60
60
  "hover:text-[var(--pu-text)] hover:bg-[var(--pu-surface-alt)] " \
61
61
  "lg:hidden transition-colors"
@@ -71,7 +71,7 @@ module Plutonium
71
71
  span(data_resource_header_target: "closeIcon", class: "hidden", aria_hidden: "true") do
72
72
  render Phlex::TablerIcons::X.new(class: "w-5 h-5")
73
73
  end
74
- span(class: "sr-only") { "Toggle sidebar" }
74
+ span(class: "sr-only") { t("plutonium.ui.layout.toggle_sidebar") }
75
75
  end
76
76
 
77
77
  def render_breadcrumbs_section
@@ -60,7 +60,7 @@ module Plutonium
60
60
  if @title
61
61
  attrs[:"aria-labelledby"] = title_id
62
62
  else
63
- attrs[:"aria-label"] = "Dialog"
63
+ attrs[:"aria-label"] = t("plutonium.ui.modal.dialog")
64
64
  end
65
65
  attrs[:"aria-describedby"] = description_id if @description.present?
66
66
  attrs
@@ -130,8 +130,8 @@ module Plutonium
130
130
  target: "_blank",
131
131
  rel: "noopener",
132
132
  class: "p-1.5 text-[var(--pu-text-muted)] hover:text-[var(--pu-text)] hover:bg-[var(--pu-surface-alt)] rounded-md transition-colors",
133
- "aria-label": "Open full page in a new tab",
134
- title: "Open full page"
133
+ "aria-label": t("plutonium.ui.modal.open_full_page_new_tab"),
134
+ title: t("plutonium.ui.modal.open_full_page")
135
135
  ) do
136
136
  render Phlex::TablerIcons::ArrowsDiagonal.new(class: "w-5 h-5")
137
137
  end
@@ -145,7 +145,7 @@ module Plutonium
145
145
  type: "button",
146
146
  class: "p-1.5 text-[var(--pu-text-muted)] hover:text-[var(--pu-text)] hover:bg-[var(--pu-surface-alt)] rounded-md transition-colors",
147
147
  data: {action: "remote-modal#close"},
148
- "aria-label": "Close dialog"
148
+ "aria-label": t("plutonium.ui.modal.close")
149
149
  ) do
150
150
  render Phlex::TablerIcons::X.new(class: "w-5 h-5")
151
151
  end
@@ -70,7 +70,7 @@ module Plutonium
70
70
  data: {resource_drop_down_target: "trigger"},
71
71
  class: "p-2 text-[var(--pu-text-muted)] rounded-[var(--pu-radius-md)] hover:text-[var(--pu-text)] hover:bg-[var(--pu-surface-alt)] focus:ring-2 focus:ring-[var(--pu-border)] transition-colors"
72
72
  ) do
73
- span(class: "sr-only") { "View #{@label}" }
73
+ span(class: "sr-only") { t("plutonium.ui.nav_grid_menu.view", label: @label) }
74
74
  render @icon.new(class: "w-6 h-6")
75
75
  end
76
76
  end
@@ -76,8 +76,8 @@ module Plutonium
76
76
  id: "user-nav-dropdown-toggle",
77
77
  data: {resource_drop_down_target: "trigger"}
78
78
  ) do
79
- span(class: "sr-only") { "Open user menu" }
80
- Avatar(@record, src: @avatar_url, size: :sm, alt: "avatar", class: "ring-2 ring-[var(--pu-border)]")
79
+ span(class: "sr-only") { t("plutonium.ui.nav_user.open_menu") }
80
+ Avatar(@record, src: @avatar_url, size: :sm, alt: t("plutonium.ui.nav_user.avatar_alt"), class: "ring-2 ring-[var(--pu-border)]")
81
81
  end
82
82
  end
83
83
 
@@ -64,11 +64,19 @@ module Plutonium
64
64
  end
65
65
 
66
66
  def render_page_header
67
- return unless page_title
67
+ return unless resolved_page_title
68
68
 
69
- PageHeader(title: page_title, description: page_description, actions: page_actions)
69
+ PageHeader(title: resolved_page_title, description: resolved_page_description, actions: page_actions)
70
70
  end
71
71
 
72
+ # Titles and descriptions may be lazy translations (a proc built by
73
+ # the definition's `t`). Every surface that renders them (the page
74
+ # header, the modal chrome) goes through these so the lookup runs in
75
+ # the request's locale, the same way inputs and labels are resolved.
76
+ def resolved_page_title = Plutonium::Translation.resolve(page_title)
77
+
78
+ def resolved_page_description = Plutonium::Translation.resolve(page_description)
79
+
72
80
  def render_toolbar
73
81
  # Implement toolbar content
74
82
  end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plutonium
4
+ module UI
5
+ module Page
6
+ # The page a registered dashboard renders: the standard page header
7
+ # (title + description from `presents`) over the card grid. Keeps the
8
+ # `render_before_*` / `render_after_*` hooks of {Base} so an app can
9
+ # subclass it and add a toolbar or footer without owning the grid.
10
+ class Dashboard < Base
11
+ def initialize(dashboard:)
12
+ @dashboard = dashboard
13
+ super(page_title: dashboard.class.label, page_description: dashboard.class.description)
14
+ end
15
+
16
+ def view_template(&)
17
+ DynaFrameContent() do
18
+ div(class: tokens("pu-dashboard-page", width_classes)) do
19
+ render_before_header
20
+ render_header
21
+ render_after_header
22
+
23
+ render_before_content
24
+ render_content
25
+ render_after_content
26
+
27
+ render_before_footer
28
+ render_footer
29
+ render_after_footer
30
+ end
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ attr_reader :dashboard
37
+
38
+ def render_content
39
+ render Plutonium::UI::Dashboard::Board.new(dashboard:)
40
+ end
41
+
42
+ # A dashboard has no resource definition to consult and is its own
43
+ # navigation root, so it never renders a breadcrumb trail.
44
+ def render_breadcrumbs? = false
45
+
46
+ def width_classes
47
+ Plutonium::UI::PageWidth.classes_for(dashboard.class.width)
48
+ end
49
+
50
+ def page_type = :dashboard
51
+ end
52
+ end
53
+ end
54
+ end
@@ -7,7 +7,7 @@ module Plutonium
7
7
  private
8
8
 
9
9
  def page_title
10
- current_definition.edit_page_title || super || "Edit #{resource_name(resource_class, 1)}"
10
+ current_definition.edit_page_title || super || t("plutonium.ui.page.edit_title", resource: resource_name(resource_class, 1))
11
11
  end
12
12
 
13
13
  def page_description
@@ -25,8 +25,8 @@ module Plutonium
25
25
  def render_modal_form
26
26
  modal_class = Plutonium::UI::Modal::Base.class_for_mode(current_definition.modal_mode)
27
27
  render modal_class.new(
28
- title: page_title,
29
- description: page_description,
28
+ title: resolved_page_title,
29
+ description: resolved_page_description,
30
30
  size: current_definition.modal_size,
31
31
  # Opens this form standalone in a NEW TAB (target=_blank), so the
32
32
  # modal — and anything already typed into it — stays put.
@@ -23,8 +23,8 @@ module Plutonium
23
23
  )
24
24
 
25
25
  render modal_class.new(
26
- title: page_title,
27
- description: page_description,
26
+ title: resolved_page_title,
27
+ description: resolved_page_description,
28
28
  size: current_interactive_action.modal_size(current_definition),
29
29
  # The action's own GET path renders the standalone branch below,
30
30
  # and opens in a NEW TAB, so the modal keeps whatever has already
@@ -7,7 +7,7 @@ module Plutonium
7
7
  private
8
8
 
9
9
  def page_title
10
- current_definition.new_page_title || super || "New #{resource_name(resource_class, 1)}"
10
+ current_definition.new_page_title || super || t("plutonium.ui.page.new_title", resource: resource_name(resource_class, 1))
11
11
  end
12
12
 
13
13
  def page_description
@@ -25,8 +25,8 @@ module Plutonium
25
25
  def render_modal_form
26
26
  modal_class = Plutonium::UI::Modal::Base.class_for_mode(current_definition.modal_mode)
27
27
  render modal_class.new(
28
- title: page_title,
29
- description: page_description,
28
+ title: resolved_page_title,
29
+ description: resolved_page_description,
30
30
  size: current_definition.modal_size,
31
31
  # Opens this form standalone in a NEW TAB (target=_blank), so the
32
32
  # modal — and anything already typed into it — stays put.