plutonium 0.64.0 → 0.66.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (344) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium/SKILL.md +44 -41
  3. data/.claude/skills/plutonium-app/SKILL.md +115 -59
  4. data/.claude/skills/plutonium-async-interactions/SKILL.md +19 -19
  5. data/.claude/skills/plutonium-auth/SKILL.md +119 -53
  6. data/.claude/skills/plutonium-behavior/SKILL.md +110 -78
  7. data/.claude/skills/plutonium-dashboard/SKILL.md +168 -0
  8. data/.claude/skills/plutonium-kanban/SKILL.md +81 -49
  9. data/.claude/skills/plutonium-resource/SKILL.md +236 -145
  10. data/.claude/skills/plutonium-tenancy/SKILL.md +104 -55
  11. data/.claude/skills/plutonium-testing/SKILL.md +130 -33
  12. data/.claude/skills/plutonium-ui/SKILL.md +171 -95
  13. data/.claude/skills/plutonium-wizard/SKILL.md +93 -82
  14. data/CHANGELOG.md +80 -0
  15. data/CLAUDE.md +282 -210
  16. data/README.md +9 -9
  17. data/SECURITY.md +1 -1
  18. data/app/assets/plutonium-charts.js +20604 -0
  19. data/app/assets/plutonium-charts.js.map +7 -0
  20. data/app/assets/plutonium-charts.min.js +41 -0
  21. data/app/assets/plutonium-charts.min.js.map +7 -0
  22. data/app/assets/plutonium.css +1 -1
  23. data/app/assets/plutonium.js +794 -629
  24. data/app/assets/plutonium.js.map +4 -4
  25. data/app/assets/plutonium.min.js +48 -48
  26. data/app/assets/plutonium.min.js.map +4 -4
  27. data/app/views/plutonium/_flash_alerts.html.erb +2 -2
  28. data/app/views/plutonium/_resource_header.html.erb +2 -2
  29. data/app/views/plutonium/_resource_sidebar.html.erb +11 -2
  30. data/app/views/plutonium/_toast.html.erb +2 -2
  31. data/app/views/rodauth/_login_form.html.erb +1 -1
  32. data/app/views/rodauth/_password_visibility.html.erb +1 -1
  33. data/app/views/rodauth/add_recovery_codes.html.erb +2 -2
  34. data/app/views/rodauth/change_login.html.erb +2 -2
  35. data/app/views/rodauth/create_account.html.erb +4 -4
  36. data/app/views/rodauth/reset_password_request.html.erb +1 -1
  37. data/app/views/rodauth/verify_account_resend.html.erb +1 -1
  38. data/app/views/rodauth/webauthn_remove.html.erb +1 -1
  39. data/config/brakeman.ignore +4 -4
  40. data/config/initializers/rabl.rb +0 -40
  41. data/config/locales/en/api_client.yml +22 -0
  42. data/config/locales/en/async.yml +34 -0
  43. data/config/locales/en/dashboard.yml +25 -0
  44. data/config/locales/en/invites.yml +37 -0
  45. data/config/locales/en/js.yml +40 -0
  46. data/config/locales/en/kanban.yml +28 -0
  47. data/config/locales/en/plutonium.yml +26 -0
  48. data/config/locales/en/positioning.yml +14 -0
  49. data/config/locales/en/profile.yml +29 -0
  50. data/config/locales/en/resource.yml +60 -0
  51. data/config/locales/en/ui.yml +93 -0
  52. data/config/locales/en/wizard.yml +46 -0
  53. data/docs/.vitepress/config.ts +34 -1
  54. data/docs/.vitepress/sync-skills.mjs +6 -3
  55. data/docs/.vitepress/theme/components/HomeHero.vue +1 -1
  56. data/docs/.vitepress/theme/components/HomeInTheBox.vue +14 -0
  57. data/docs/.vitepress/theme/custom.css +34 -377
  58. data/docs/blog/introducing-plutonium-dashboards.md +174 -0
  59. data/docs/blog/introducing-plutonium-i18n.md +90 -0
  60. data/docs/blog/introducing-plutonium.md +4 -5
  61. data/docs/blog/whats-new-async-kanban-wizards.md +1 -1
  62. data/docs/getting-started/installation.md +5 -5
  63. data/docs/getting-started/tutorial/02-first-resource.md +3 -3
  64. data/docs/getting-started/tutorial/03-authentication.md +7 -7
  65. data/docs/getting-started/tutorial/04-authorization.md +21 -4
  66. data/docs/getting-started/tutorial/05-custom-actions.md +2 -2
  67. data/docs/getting-started/tutorial/06-nested-resources.md +5 -2
  68. data/docs/getting-started/tutorial/07-author-portal.md +2 -2
  69. data/docs/getting-started/tutorial/08-customizing-ui.md +45 -30
  70. data/docs/getting-started/tutorial/index.md +1 -1
  71. data/docs/guides/adding-resources.md +10 -7
  72. data/docs/guides/authentication.md +25 -25
  73. data/docs/guides/authorization.md +24 -24
  74. data/docs/guides/creating-packages.md +17 -17
  75. data/docs/guides/custom-actions.md +32 -32
  76. data/docs/guides/customizing-ui.md +29 -26
  77. data/docs/guides/dashboards.md +300 -0
  78. data/docs/guides/index.md +4 -3
  79. data/docs/guides/kanban.md +56 -56
  80. data/docs/guides/multi-tenancy.md +35 -22
  81. data/docs/guides/nested-resources.md +21 -21
  82. data/docs/guides/performance.md +3 -3
  83. data/docs/guides/search-filtering.md +13 -13
  84. data/docs/guides/testing.md +16 -12
  85. data/docs/guides/theming.md +32 -17
  86. data/docs/guides/troubleshooting.md +2 -2
  87. data/docs/guides/user-invites.md +17 -17
  88. data/docs/guides/user-profile.md +51 -24
  89. data/docs/guides/wizards.md +55 -55
  90. data/docs/public/images/blog/dashboards-card-error.png +0 -0
  91. data/docs/public/images/blog/dashboards-loading.png +0 -0
  92. data/docs/public/images/blog/dashboards-overview.png +0 -0
  93. data/docs/public/images/blog/dashboards-sidebar.png +0 -0
  94. data/docs/public/images/blog/i18n-es-admin.png +0 -0
  95. data/docs/public/images/guides/dashboard-card-error.png +0 -0
  96. data/docs/public/images/guides/dashboard-loading.png +0 -0
  97. data/docs/public/images/guides/dashboard-overview.png +0 -0
  98. data/docs/public/images/guides/dashboard-sidebar.png +0 -0
  99. data/docs/reference/app/generators.md +24 -24
  100. data/docs/reference/app/index.md +15 -18
  101. data/docs/reference/app/packages.md +8 -8
  102. data/docs/reference/app/portals.md +77 -29
  103. data/docs/reference/auth/accounts.md +15 -15
  104. data/docs/reference/auth/index.md +12 -12
  105. data/docs/reference/auth/profile.md +67 -29
  106. data/docs/reference/behavior/async-interactions.md +24 -24
  107. data/docs/reference/behavior/controllers.md +28 -28
  108. data/docs/reference/behavior/index.md +5 -5
  109. data/docs/reference/behavior/interactions.md +44 -44
  110. data/docs/reference/behavior/policies.md +48 -28
  111. data/docs/reference/configuration.md +88 -31
  112. data/docs/reference/dashboard/dsl.md +123 -0
  113. data/docs/reference/dashboard/index.md +35 -0
  114. data/docs/reference/dashboard/registration.md +76 -0
  115. data/docs/reference/generators/lite.md +7 -7
  116. data/docs/reference/i18n.md +262 -0
  117. data/docs/reference/index.md +11 -2
  118. data/docs/reference/kanban/authorization.md +9 -9
  119. data/docs/reference/kanban/dsl.md +33 -33
  120. data/docs/reference/kanban/index.md +1 -1
  121. data/docs/reference/kanban/positioning.md +18 -16
  122. data/docs/reference/resource/actions.md +51 -51
  123. data/docs/reference/resource/definition.md +166 -88
  124. data/docs/reference/resource/export.md +11 -7
  125. data/docs/reference/resource/index.md +16 -14
  126. data/docs/reference/resource/model.md +24 -24
  127. data/docs/reference/{positioning.md → resource/positioning.md} +78 -75
  128. data/docs/reference/resource/query.md +14 -12
  129. data/docs/reference/tenancy/entity-scoping.md +65 -35
  130. data/docs/reference/tenancy/index.md +11 -11
  131. data/docs/reference/tenancy/invites.md +20 -20
  132. data/docs/reference/tenancy/nested-resources.md +13 -13
  133. data/docs/reference/testing/index.md +116 -22
  134. data/docs/reference/ui/assets.md +65 -25
  135. data/docs/reference/ui/components.md +20 -20
  136. data/docs/reference/ui/displays.md +19 -22
  137. data/docs/reference/ui/forms.md +35 -35
  138. data/docs/reference/ui/index.md +17 -15
  139. data/docs/reference/ui/layouts.md +21 -21
  140. data/docs/reference/ui/pages.md +22 -22
  141. data/docs/reference/ui/tables.md +7 -7
  142. data/docs/reference/wizard/anchoring-resume.md +33 -32
  143. data/docs/reference/wizard/dsl.md +45 -45
  144. data/docs/reference/wizard/index.md +6 -6
  145. data/docs/reference/wizard/one-time.md +18 -18
  146. data/docs/reference/wizard/registration-launch.md +32 -32
  147. data/docs/reference/wizard/storage-config.md +23 -23
  148. data/esbuild.config.js +5 -2
  149. data/gemfiles/rails_7.gemfile.lock +2 -2
  150. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  151. data/lib/active_model/validations/array_validator.rb +1 -1
  152. data/lib/active_model/validations/attached_validator.rb +1 -1
  153. data/lib/active_model/validations/url_validator.rb +2 -2
  154. data/lib/generators/pu/core/assets/assets_generator.rb +36 -21
  155. data/lib/generators/pu/core/assets/templates/postcss.config.js +1 -1
  156. data/lib/generators/pu/core/update/update_generator.rb +3 -2
  157. data/lib/generators/pu/dashboard/dashboard_generator.rb +96 -0
  158. data/lib/generators/pu/dashboard/templates/dashboard.rb.tt +25 -0
  159. data/lib/generators/pu/docker/install/install_generator.rb +58 -0
  160. data/lib/generators/pu/docker/install/templates/Dockerfile.dev.tt +18 -3
  161. data/lib/generators/pu/docker/install/templates/Dockerfile.tt +20 -5
  162. data/lib/generators/pu/docker/install/templates/{docker-compose.yml → docker-compose.yml.tt} +1 -1
  163. data/lib/generators/pu/gem/dotenv/dotenv_generator.rb +1 -1
  164. data/lib/generators/pu/invites/invitable_generator.rb +18 -1
  165. data/lib/generators/pu/lib/plutonium_generators/concerns/actions.rb +0 -95
  166. data/lib/generators/pu/lib/plutonium_generators/concerns/js_package_manager.rb +146 -0
  167. data/lib/generators/pu/lib/plutonium_generators/generator.rb +1 -17
  168. data/lib/generators/pu/pkg/package/package_generator.rb +1 -0
  169. data/lib/generators/pu/pkg/package/templates/config/locales/en.yml.tt +28 -0
  170. data/lib/generators/pu/pkg/portal/portal_generator.rb +1 -0
  171. data/lib/generators/pu/pkg/portal/templates/config/locales/en.yml.tt +30 -0
  172. data/lib/generators/pu/profile/conn_generator.rb +6 -0
  173. data/lib/generators/pu/res/scaffold/scaffold_generator.rb +0 -8
  174. data/lib/generators/pu/rodauth/concerns/feature_selector.rb +0 -18
  175. data/lib/plutonium/action/base.rb +27 -3
  176. data/lib/plutonium/action/interactive.rb +17 -4
  177. data/lib/plutonium/api_client/concerns/create_api_client.rb +12 -11
  178. data/lib/plutonium/api_client/concerns/disable_api_client.rb +3 -3
  179. data/lib/plutonium/auth/sequel_adapter.rb +1 -1
  180. data/lib/plutonium/configuration.rb +5 -0
  181. data/lib/plutonium/core/controller.rb +25 -1
  182. data/lib/plutonium/core/controllers/authorizable.rb +1 -1
  183. data/lib/plutonium/dashboard/base.rb +114 -0
  184. data/lib/plutonium/dashboard/card.rb +200 -0
  185. data/lib/plutonium/dashboard/controller.rb +101 -0
  186. data/lib/plutonium/dashboard/dsl.rb +104 -0
  187. data/lib/plutonium/dashboard/register.rb +40 -0
  188. data/lib/plutonium/dashboard/route_resolution.rb +30 -0
  189. data/lib/plutonium/dashboard.rb +42 -0
  190. data/lib/plutonium/definition/actions.rb +6 -1
  191. data/lib/plutonium/definition/base.rb +41 -3
  192. data/lib/plutonium/definition/index_views.rb +1 -1
  193. data/lib/plutonium/definition/positioning.rb +1 -1
  194. data/lib/plutonium/definition/presentable.rb +2 -2
  195. data/lib/plutonium/helpers/assets_helper.rb +15 -2
  196. data/lib/plutonium/helpers/display_helper.rb +10 -2
  197. data/lib/plutonium/interaction/async/context.rb +6 -8
  198. data/lib/plutonium/interaction/async/executor.rb +6 -7
  199. data/lib/plutonium/interaction/async/run.rb +2 -2
  200. data/lib/plutonium/interaction/async/run_definition.rb +1 -1
  201. data/lib/plutonium/interaction/base.rb +2 -0
  202. data/lib/plutonium/interaction/concerns/dispatchable.rb +15 -3
  203. data/lib/plutonium/invites/concerns/cancel_invite.rb +3 -3
  204. data/lib/plutonium/invites/concerns/invite_token.rb +25 -7
  205. data/lib/plutonium/invites/concerns/invite_user.rb +10 -5
  206. data/lib/plutonium/invites/concerns/resend_invite.rb +4 -4
  207. data/lib/plutonium/invites/controller.rb +11 -11
  208. data/lib/plutonium/kanban/column.rb +13 -3
  209. data/lib/plutonium/kanban/dsl.rb +7 -4
  210. data/lib/plutonium/models/has_cents.rb +1 -1
  211. data/lib/plutonium/profile/security_section.rb +10 -18
  212. data/lib/plutonium/query/filter.rb +10 -1
  213. data/lib/plutonium/query/filters/association.rb +27 -2
  214. data/lib/plutonium/query/filters/boolean.rb +4 -4
  215. data/lib/plutonium/query/filters/date.rb +4 -17
  216. data/lib/plutonium/query/filters/date_range.rb +2 -2
  217. data/lib/plutonium/query/filters/select.rb +10 -1
  218. data/lib/plutonium/query/filters/text.rb +9 -12
  219. data/lib/plutonium/railtie.rb +17 -0
  220. data/lib/plutonium/resource/controller.rb +10 -5
  221. data/lib/plutonium/resource/controllers/crud_actions.rb +17 -20
  222. data/lib/plutonium/resource/controllers/export_csv.rb +19 -9
  223. data/lib/plutonium/resource/controllers/kanban_actions.rb +16 -16
  224. data/lib/plutonium/resource/controllers/position_actions.rb +5 -5
  225. data/lib/plutonium/resource/controllers/typeahead.rb +24 -1
  226. data/lib/plutonium/resource/query_object.rb +30 -3
  227. data/lib/plutonium/resource/record/associated_with.rb +23 -2
  228. data/lib/plutonium/routing/dashboard_registration.rb +137 -0
  229. data/lib/plutonium/routing/resource_registration.rb +5 -0
  230. data/lib/plutonium/routing/route_set_extensions.rb +2 -1
  231. data/lib/plutonium/translation.rb +227 -0
  232. data/lib/plutonium/ui/actions_dropdown.rb +2 -2
  233. data/lib/plutonium/ui/breadcrumbs.rb +3 -3
  234. data/lib/plutonium/ui/color_mode_selector.rb +1 -1
  235. data/lib/plutonium/ui/component/behaviour.rb +46 -0
  236. data/lib/plutonium/ui/component/methods.rb +4 -0
  237. data/lib/plutonium/ui/dashboard/board.rb +80 -0
  238. data/lib/plutonium/ui/dashboard/card.rb +127 -0
  239. data/lib/plutonium/ui/dashboard/chart.rb +43 -0
  240. data/lib/plutonium/ui/dashboard/custom.rb +32 -0
  241. data/lib/plutonium/ui/dashboard/frame.rb +25 -0
  242. data/lib/plutonium/ui/dashboard/metric.rb +137 -0
  243. data/lib/plutonium/ui/dashboard/skeleton.rb +40 -0
  244. data/lib/plutonium/ui/display/components/attachment.rb +1 -1
  245. data/lib/plutonium/ui/display/components/badge.rb +7 -1
  246. data/lib/plutonium/ui/display/components/boolean.rb +2 -2
  247. data/lib/plutonium/ui/display/resource.rb +23 -16
  248. data/lib/plutonium/ui/export_button.rb +3 -3
  249. data/lib/plutonium/ui/form/components/key_value_store.rb +5 -5
  250. data/lib/plutonium/ui/form/components/password.rb +2 -2
  251. data/lib/plutonium/ui/form/components/resource_select.rb +27 -0
  252. data/lib/plutonium/ui/form/components/secure_association.rb +2 -0
  253. data/lib/plutonium/ui/form/components/uppy.rb +11 -3
  254. data/lib/plutonium/ui/form/concerns/renders_nested_resource_fields.rb +1 -1
  255. data/lib/plutonium/ui/form/concerns/renders_repeater_row_controls.rb +5 -4
  256. data/lib/plutonium/ui/form/concerns/renders_structured_inputs.rb +1 -1
  257. data/lib/plutonium/ui/form/concerns/typeahead_attributes.rb +7 -1
  258. data/lib/plutonium/ui/form/query.rb +8 -8
  259. data/lib/plutonium/ui/form/resource.rb +54 -26
  260. data/lib/plutonium/ui/frame_navigator_panel.rb +4 -4
  261. data/lib/plutonium/ui/grid/card.rb +3 -3
  262. data/lib/plutonium/ui/grid/resource.rb +3 -3
  263. data/lib/plutonium/ui/interaction/async/run_progress.rb +6 -6
  264. data/lib/plutonium/ui/interaction/async/running_banner.rb +2 -2
  265. data/lib/plutonium/ui/kanban/column.rb +6 -6
  266. data/lib/plutonium/ui/layout/base.rb +19 -1
  267. data/lib/plutonium/ui/layout/header.rb +1 -1
  268. data/lib/plutonium/ui/layout/icon_rail.rb +4 -4
  269. data/lib/plutonium/ui/layout/rodauth_layout.rb +1 -1
  270. data/lib/plutonium/ui/layout/sidebar.rb +1 -1
  271. data/lib/plutonium/ui/layout/topbar.rb +2 -2
  272. data/lib/plutonium/ui/modal/base.rb +4 -4
  273. data/lib/plutonium/ui/nav_grid_menu.rb +1 -1
  274. data/lib/plutonium/ui/nav_user.rb +2 -2
  275. data/lib/plutonium/ui/page/base.rb +10 -2
  276. data/lib/plutonium/ui/page/dashboard.rb +54 -0
  277. data/lib/plutonium/ui/page/edit.rb +3 -3
  278. data/lib/plutonium/ui/page/interactive_action.rb +2 -2
  279. data/lib/plutonium/ui/page/new.rb +3 -3
  280. data/lib/plutonium/ui/page/show.rb +2 -2
  281. data/lib/plutonium/ui/page/wizard.rb +22 -16
  282. data/lib/plutonium/ui/page/wizard_chooser.rb +10 -10
  283. data/lib/plutonium/ui/page/wizard_completed.rb +2 -2
  284. data/lib/plutonium/ui/skeleton_table.rb +1 -1
  285. data/lib/plutonium/ui/table/base.rb +2 -2
  286. data/lib/plutonium/ui/table/components/attachment.rb +1 -1
  287. data/lib/plutonium/ui/table/components/bulk_actions_toolbar.rb +19 -3
  288. data/lib/plutonium/ui/table/components/drag_handle.rb +3 -3
  289. data/lib/plutonium/ui/table/components/filter_form.rb +11 -10
  290. data/lib/plutonium/ui/table/components/filter_pills.rb +4 -4
  291. data/lib/plutonium/ui/table/components/pagy_info.rb +61 -17
  292. data/lib/plutonium/ui/table/components/pagy_pagination.rb +9 -5
  293. data/lib/plutonium/ui/table/components/row_actions_dropdown.rb +1 -1
  294. data/lib/plutonium/ui/table/components/scopes_bar.rb +6 -2
  295. data/lib/plutonium/ui/table/components/scopes_pills.rb +3 -3
  296. data/lib/plutonium/ui/table/components/selection_column.rb +1 -1
  297. data/lib/plutonium/ui/table/components/toolbar.rb +2 -2
  298. data/lib/plutonium/ui/table/components/view_switcher.rb +12 -8
  299. data/lib/plutonium/ui/table/resource.rb +53 -17
  300. data/lib/plutonium/ui/wizard/review.rb +7 -7
  301. data/lib/plutonium/ui/wizard/stepper.rb +1 -1
  302. data/lib/plutonium/ui/wizard/summary_display.rb +6 -0
  303. data/lib/plutonium/version.rb +1 -1
  304. data/lib/plutonium/wizard/base.rb +2 -0
  305. data/lib/plutonium/wizard/driving.rb +8 -2
  306. data/lib/plutonium/wizard/dsl.rb +4 -2
  307. data/lib/plutonium/wizard/field_importer.rb +1 -8
  308. data/lib/plutonium/wizard/review_step.rb +8 -1
  309. data/lib/plutonium/wizard/step.rb +14 -3
  310. data/package.json +4 -2
  311. data/plutonium.gemspec +4 -1
  312. data/postcss-gem-import.cjs +34 -0
  313. data/postcss-gem-import.js +3 -28
  314. data/src/css/components.css +101 -10
  315. data/src/css/slim_select.css +0 -38
  316. data/src/css/tokens.css +24 -0
  317. data/src/js/controllers/attachment_input_controller.js +15 -6
  318. data/src/js/controllers/chart_controller.js +162 -0
  319. data/src/js/controllers/clipboard_controller.js +3 -2
  320. data/src/js/controllers/dirty_form_guard_controller.js +4 -3
  321. data/src/js/controllers/easymde_controller.js +3 -0
  322. data/src/js/controllers/filter_panel_controller.js +15 -6
  323. data/src/js/controllers/flatpickr_controller.js +8 -0
  324. data/src/js/controllers/frame_refresh_controller.js +59 -0
  325. data/src/js/controllers/intl_tel_input_controller.js +5 -0
  326. data/src/js/controllers/register_controllers.js +4 -0
  327. data/src/js/controllers/slim_select_controller.js +79 -176
  328. data/src/js/core.js +9 -1
  329. data/src/js/i18n.js +69 -0
  330. data/src/js/plutonium-charts.js +15 -0
  331. data/src/js/turbo/turbo_confirm.js +5 -3
  332. metadata +64 -16
  333. data/.yarnrc.yml +0 -8
  334. data/config/initializers/hotwire_turbo_monkey_patches.rb +0 -12
  335. data/lib/generators/pu/field/input/input_generator.rb +0 -32
  336. data/lib/generators/pu/field/input/templates/input.rb.tt +0 -15
  337. data/lib/generators/pu/lib/plutonium_generators/concerns/config.rb +0 -39
  338. data/lib/generators/pu/lib/plutonium_generators/concerns/serializer.rb +0 -39
  339. data/lib/generators/pu/lib/plutonium_generators/installer.rb +0 -205
  340. data/lib/generators/pu/res/scaffold/templates/presenter.rb.tt +0 -13
  341. data/lib/generators/pu/res/scaffold/templates/query_object.rb.tt +0 -17
  342. data/public/plutonium-assets/application.js +0 -31419
  343. data/public/plutonium-assets/plutonium.ico +0 -0
  344. data/yarn.lock +0 -6858
@@ -612,6 +612,94 @@ select.pu-input[multiple] option {
612
612
  @apply mt-1 text-base;
613
613
  }
614
614
 
615
+ /* ===================
616
+ DASHBOARD CARDS
617
+ =================== */
618
+
619
+ .pu-dashboard-card {
620
+ @apply p-4;
621
+ }
622
+
623
+ .pu-dashboard-card-header {
624
+ @apply flex items-start gap-3;
625
+ }
626
+
627
+ .pu-dashboard-card-icon {
628
+ background-color: var(--pu-surface-alt);
629
+ color: var(--pu-text-muted);
630
+ border-radius: var(--pu-radius-md);
631
+ @apply flex size-9 shrink-0 items-center justify-center;
632
+ }
633
+
634
+ .pu-dashboard-card-title {
635
+ color: var(--pu-text-muted);
636
+ @apply text-sm font-medium leading-5 truncate;
637
+ }
638
+
639
+ .pu-dashboard-card-description {
640
+ color: var(--pu-text-subtle);
641
+ @apply mt-0.5 text-xs;
642
+ }
643
+
644
+ .pu-dashboard-card-link {
645
+ color: var(--pu-text-subtle);
646
+ @apply shrink-0 -mr-1 -mt-1 rounded-[var(--pu-radius-md)] p-1 transition-colors hover:text-[var(--pu-text)] hover:bg-[var(--pu-surface-alt)];
647
+ }
648
+
649
+ .pu-dashboard-card-body {
650
+ @apply mt-3 flex-1 min-w-0;
651
+ }
652
+
653
+ .pu-dashboard-card-error {
654
+ color: var(--pu-text-danger);
655
+ @apply flex items-center gap-2 text-sm;
656
+ }
657
+
658
+ .pu-dashboard-skeleton {
659
+ @apply p-4;
660
+ }
661
+
662
+ .pu-metric-value {
663
+ color: var(--pu-text);
664
+ @apply text-3xl font-semibold leading-none tracking-tight tabular-nums truncate;
665
+ }
666
+
667
+ .pu-metric-change {
668
+ @apply mt-3 flex items-center gap-1.5 text-sm;
669
+ }
670
+
671
+ .pu-metric-change-label {
672
+ color: var(--pu-text-muted);
673
+ @apply font-normal;
674
+ }
675
+
676
+ .pu-metric-change-positive {
677
+ @apply text-success-600;
678
+ }
679
+ .dark .pu-metric-change-positive {
680
+ @apply text-success-400;
681
+ }
682
+
683
+ .pu-metric-change-negative {
684
+ @apply text-danger-600;
685
+ }
686
+ .dark .pu-metric-change-negative {
687
+ @apply text-danger-400;
688
+ }
689
+
690
+ .pu-metric-change-neutral {
691
+ color: var(--pu-text-muted);
692
+ }
693
+
694
+ .pu-chart {
695
+ @apply relative w-full;
696
+ }
697
+
698
+ .pu-chart-loading {
699
+ color: var(--pu-text-subtle);
700
+ @apply absolute inset-0 flex items-center justify-center text-sm;
701
+ }
702
+
615
703
  /* ===================
616
704
  SELECTION CELL
617
705
  =================== */
@@ -883,7 +971,7 @@ html.pu-rail-pinned .icon-rail-pin-expand {
883
971
  @apply flex items-center p-4 mb-4 rounded-lg;
884
972
  }
885
973
  .dark .pu-alert {
886
- @apply bg-stone-300;
974
+ @apply bg-gray-800;
887
975
  }
888
976
 
889
977
  .pu-alert-message {
@@ -894,7 +982,7 @@ html.pu-rail-pinned .icon-rail-pin-expand {
894
982
  @apply ms-auto -mx-1.5 -my-1.5 rounded-lg focus:ring-2 p-1.5 inline-flex items-center justify-center h-8 w-8;
895
983
  }
896
984
  .dark .pu-alert-close {
897
- @apply bg-stone-300 hover:bg-gray-700;
985
+ @apply bg-gray-800 hover:bg-gray-700;
898
986
  }
899
987
 
900
988
  /* --- Variants: success / warning / danger / info ---
@@ -907,11 +995,11 @@ html.pu-rail-pinned .icon-rail-pin-expand {
907
995
  .pu-toast-success .pu-toast-icon { @apply text-success-500 bg-success-100; }
908
996
  .dark .pu-toast-success .pu-toast-icon { @apply bg-success-800 text-success-200; }
909
997
  .pu-toast-success .pu-toast-close { @apply bg-success-50 text-success-400 hover:bg-success-200 focus:ring-success-400; }
910
- .dark .pu-toast-success .pu-toast-close { @apply text-success-400; }
998
+ .dark .pu-toast-success .pu-toast-close { @apply bg-gray-800 text-success-400 hover:bg-gray-700; }
911
999
  .pu-alert-success { @apply bg-success-50 text-success-800; }
912
1000
  .dark .pu-alert-success { @apply text-success-400; }
913
1001
  .pu-alert-success .pu-alert-close { @apply bg-success-50 text-success-500 hover:bg-success-200 focus:ring-success-400; }
914
- .dark .pu-alert-success .pu-alert-close { @apply text-success-400; }
1002
+ .dark .pu-alert-success .pu-alert-close { @apply bg-gray-800 text-success-400 hover:bg-gray-700; }
915
1003
 
916
1004
  /* warning */
917
1005
  .pu-toast-warning { @apply bg-warning-50; }
@@ -919,11 +1007,11 @@ html.pu-rail-pinned .icon-rail-pin-expand {
919
1007
  .pu-toast-warning .pu-toast-icon { @apply text-warning-500 bg-warning-100; }
920
1008
  .dark .pu-toast-warning .pu-toast-icon { @apply bg-warning-800 text-warning-200; }
921
1009
  .pu-toast-warning .pu-toast-close { @apply bg-warning-50 text-warning-400 hover:bg-warning-200 focus:ring-warning-400; }
922
- .dark .pu-toast-warning .pu-toast-close { @apply text-warning-400; }
1010
+ .dark .pu-toast-warning .pu-toast-close { @apply bg-gray-800 text-warning-400 hover:bg-gray-700; }
923
1011
  .pu-alert-warning { @apply bg-warning-50 text-warning-800; }
924
1012
  .dark .pu-alert-warning { @apply text-warning-400; }
925
1013
  .pu-alert-warning .pu-alert-close { @apply bg-warning-50 text-warning-500 hover:bg-warning-200 focus:ring-warning-400; }
926
- .dark .pu-alert-warning .pu-alert-close { @apply text-warning-400; }
1014
+ .dark .pu-alert-warning .pu-alert-close { @apply bg-gray-800 text-warning-400 hover:bg-gray-700; }
927
1015
 
928
1016
  /* danger */
929
1017
  .pu-toast-danger { @apply bg-danger-50; }
@@ -931,11 +1019,11 @@ html.pu-rail-pinned .icon-rail-pin-expand {
931
1019
  .pu-toast-danger .pu-toast-icon { @apply text-danger-500 bg-danger-100; }
932
1020
  .dark .pu-toast-danger .pu-toast-icon { @apply bg-danger-800 text-danger-200; }
933
1021
  .pu-toast-danger .pu-toast-close { @apply bg-danger-50 text-danger-400 hover:bg-danger-200 focus:ring-danger-400; }
934
- .dark .pu-toast-danger .pu-toast-close { @apply text-danger-400; }
1022
+ .dark .pu-toast-danger .pu-toast-close { @apply bg-gray-800 text-danger-400 hover:bg-gray-700; }
935
1023
  .pu-alert-danger { @apply bg-danger-50 text-danger-800; }
936
1024
  .dark .pu-alert-danger { @apply text-danger-400; }
937
1025
  .pu-alert-danger .pu-alert-close { @apply bg-danger-50 text-danger-500 hover:bg-danger-200 focus:ring-danger-400; }
938
- .dark .pu-alert-danger .pu-alert-close { @apply text-danger-400; }
1026
+ .dark .pu-alert-danger .pu-alert-close { @apply bg-gray-800 text-danger-400 hover:bg-gray-700; }
939
1027
 
940
1028
  /* info (default / :notice) */
941
1029
  .pu-toast-info { @apply bg-info-50; }
@@ -943,11 +1031,11 @@ html.pu-rail-pinned .icon-rail-pin-expand {
943
1031
  .pu-toast-info .pu-toast-icon { @apply text-info-500 bg-info-100; }
944
1032
  .dark .pu-toast-info .pu-toast-icon { @apply bg-info-800 text-info-200; }
945
1033
  .pu-toast-info .pu-toast-close { @apply bg-info-50 text-info-400 hover:bg-info-200 focus:ring-info-400; }
946
- .dark .pu-toast-info .pu-toast-close { @apply text-info-400; }
1034
+ .dark .pu-toast-info .pu-toast-close { @apply bg-gray-800 text-info-400 hover:bg-gray-700; }
947
1035
  .pu-alert-info { @apply bg-info-50 text-info-800; }
948
1036
  .dark .pu-alert-info { @apply text-info-400; }
949
1037
  .pu-alert-info .pu-alert-close { @apply bg-info-50 text-info-500 hover:bg-info-200 focus:ring-info-400; }
950
- .dark .pu-alert-info .pu-alert-close { @apply text-info-400; }
1038
+ .dark .pu-alert-info .pu-alert-close { @apply bg-gray-800 text-info-400 hover:bg-gray-700; }
951
1039
 
952
1040
  /* ===========================================================================
953
1041
  Wizard — horizontal "steps" stepper (numbered nodes on a connector track)
@@ -975,6 +1063,9 @@ html.pu-rail-pinned .icon-rail-pin-expand {
975
1063
  }
976
1064
  .pu-wizard-steps .pu-step-label {
977
1065
  @apply block mt-2 font-bold; font-size: .8rem; color: var(--pu-text-muted);
1066
+ /* A 13+ step wizard gives each label a narrow column; without this a single
1067
+ long word ("organisation") overflows and collides with its neighbours. */
1068
+ overflow-wrap: anywhere; hyphens: auto;
978
1069
  }
979
1070
  .pu-wizard-steps a.pu-step-link:hover .pu-step-label { @apply underline; }
980
1071
 
@@ -399,41 +399,3 @@
399
399
  .ss-main.ss-valid .ss-values .ss-placeholder {
400
400
  @apply text-success-400;
401
401
  }
402
-
403
- /* Modal-specific styles for SlimSelect dropdown */
404
- .ss-dropdown-container {
405
- position: absolute;
406
- z-index: 9999;
407
- inset: 40% 0px auto;
408
- }
409
-
410
- .ss-dropdown-container .ss-content {
411
- position: static !important;
412
- transform: none !important;
413
- width: 100% !important;
414
- border-radius: 0 !important;
415
- margin: 0 !important;
416
- pointer-events: none !important;
417
- /* Disabled by default */
418
- }
419
-
420
- /* When active (dropdown is expanded), enable pointer events */
421
- .ss-dropdown-container.ss-active .ss-content {
422
- pointer-events: auto !important;
423
- }
424
-
425
- .ss-dropdown-container .ss-list {
426
- max-height: 250px !important;
427
- overflow-y: auto !important;
428
- }
429
-
430
- /* Ensure the dropdown doesn't block other elements when closed */
431
- .ss-dropdown-container:not(:has(.ss-content)),
432
- .ss-dropdown-container:not(.ss-active) {
433
- pointer-events: none !important;
434
- }
435
-
436
- /* Prevent interaction with closed dropdown */
437
- .ss-dropdown-container:not(.ss-active) * {
438
- pointer-events: none !important;
439
- }
data/src/css/tokens.css CHANGED
@@ -57,6 +57,18 @@
57
57
  --pu-card-bg: #ffffff;
58
58
  --pu-card-border: #e2e8f0;
59
59
 
60
+ /* ===================
61
+ Chart Palette - series colors, read by the chart controller
62
+ =================== */
63
+ --pu-chart-1: theme(colors.primary.500);
64
+ --pu-chart-2: #0ea5e9;
65
+ --pu-chart-3: #10b981;
66
+ --pu-chart-4: #f59e0b;
67
+ --pu-chart-5: #ef4444;
68
+ --pu-chart-6: #8b5cf6;
69
+ --pu-chart-7: #ec4899;
70
+ --pu-chart-8: #64748b;
71
+
60
72
  /* ===================
61
73
  Shadow System - Layered, soft
62
74
  =================== */
@@ -150,6 +162,18 @@
150
162
  --pu-card-bg: #1e293b;
151
163
  --pu-card-border: #334155;
152
164
 
165
+ /* ===================
166
+ Chart Palette (Dark) - lighter tints for contrast on dark surfaces
167
+ =================== */
168
+ --pu-chart-1: theme(colors.primary.400);
169
+ --pu-chart-2: #38bdf8;
170
+ --pu-chart-3: #34d399;
171
+ --pu-chart-4: #fbbf24;
172
+ --pu-chart-5: #f87171;
173
+ --pu-chart-6: #a78bfa;
174
+ --pu-chart-7: #f472b6;
175
+ --pu-chart-8: #94a3b8;
176
+
153
177
  /* ===================
154
178
  Shadow System (Dark)
155
179
  =================== */
@@ -5,6 +5,7 @@ import Dashboard from '@uppy/dashboard'
5
5
  import ImageEditor from '@uppy/image-editor'
6
6
  import XHRUpload from '@uppy/xhr-upload'
7
7
  import DomElement from "../support/dom_element"
8
+ import { t } from "../i18n.js"
8
9
 
9
10
  // Connects to data-controller="attachment-input"
10
11
  export default class extends Controller {
@@ -109,7 +110,7 @@ export default class extends Controller {
109
110
  const dialog = this.element.closest("dialog")
110
111
  if (dialog) dashboardOptions.target = dialog
111
112
 
112
- this.uppy = new Uppy({
113
+ const uppyOptions = {
113
114
  restrictions: {
114
115
  maxFileSize: this.maxFileSizeValue,
115
116
  minFileSize: this.minFileSizeValue,
@@ -119,7 +120,14 @@ export default class extends Controller {
119
120
  allowedFileTypes: this.allowedFileTypesValue,
120
121
  requiredMetaFields: this.requiredMetaFieldsValue,
121
122
  }
122
- })
123
+ }
124
+
125
+ // Optional locale passthrough: plutonium.js.libraries.uppy.strings holds
126
+ // Uppy locale strings (core and Dashboard), merged over the English defaults.
127
+ const strings = t("plutonium.js.libraries.uppy.strings")
128
+ if (strings && typeof strings === "object") uppyOptions.locale = { strings }
129
+
130
+ this.uppy = new Uppy(uppyOptions)
123
131
  .use(Dashboard, dashboardOptions)
124
132
  .use(ImageEditor, { target: Dashboard })
125
133
 
@@ -176,7 +184,7 @@ export default class extends Controller {
176
184
  const len = this.attachmentPreviewOutlets.length
177
185
  if (len > 1) {
178
186
  this.deleteAllTrigger.style["display"] = 'initial'
179
- this.deleteAllTrigger.textContent = `Delete ${this.attachmentPreviewOutlets.length}`
187
+ this.deleteAllTrigger.textContent = t("plutonium.js.attachment_input.delete_all", { count: this.attachmentPreviewOutlets.length })
180
188
  } else {
181
189
  this.deleteAllTrigger.style["display"] = 'none'
182
190
  }
@@ -198,7 +206,8 @@ export default class extends Controller {
198
206
  }
199
207
 
200
208
  #buildUploadTrigger() {
201
- const triggerPrompt = this.multiple ? "Choose files" : "Choose file"
209
+ // One plural-keyed string: `one` for a single attachment, `other` for multiple.
210
+ const triggerPrompt = t("plutonium.js.attachment_input.choose_file", { count: this.multiple ? 2 : 1 })
202
211
  this.uploadTrigger = DomElement.fromTemplate(
203
212
  `<button type="button" class="text-gray-900 bg-white border border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700 dark:hover:border-gray-600 dark:focus:ring-gray-700 inline-flex items-center">
204
213
  <svg class="w-4 h-4 mr-2" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 16">
@@ -214,12 +223,12 @@ export default class extends Controller {
214
223
  #buildDeleteAllTrigger() {
215
224
  this.deleteAllTrigger = DomElement.fromTemplate(
216
225
  `<button type="button" class="text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-red-600 dark:hover:bg-red-700 focus:outline-none dark:focus:ring-red-800 inline-flex items-center">
217
- Delete ${this.attachmentPreviewOutlets.length}
226
+ ${t("plutonium.js.attachment_input.delete_all", { count: this.attachmentPreviewOutlets.length })}
218
227
  </button>`,
219
228
  false
220
229
  )
221
230
  this.deleteAllTrigger.addEventListener('click', () => {
222
- if (confirm('Are you sure?')) this.attachmentPreviewContainerOutlet.clear()
231
+ if (confirm(t("plutonium.js.are_you_sure"))) this.attachmentPreviewContainerOutlet.clear()
223
232
  })
224
233
  }
225
234
 
@@ -0,0 +1,162 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import { t } from "../i18n.js"
3
+
4
+ // Connects to data-controller="chart"
5
+ //
6
+ // Draws a Chartkick chart from data attributes rendered by
7
+ // Plutonium::UI::Dashboard::Chart:
8
+ //
9
+ // data-chart-type-value — Chartkick constructor ("LineChart", "PieChart", ...)
10
+ // data-chart-data-value — JSON: {label: value}, [[label, value], ...] or [{name, data}, ...]
11
+ // data-chart-options-value — JSON: Chartkick options passed through from the card
12
+ // data-chart-script-value — URL of the charts bundle, injected once per page
13
+ //
14
+ // The bundle is loaded on demand so pages without a chart never pay for
15
+ // Chart.js. Colors and text default to Plutonium's design tokens and are
16
+ // re-read when the color mode flips, so a chart matches the theme in both modes.
17
+
18
+ const PALETTE_SIZE = 8
19
+ let libraryPromise = null
20
+
21
+ function loadLibrary(src) {
22
+ if (window.Chartkick && window.Chartkick.adapters.length > 0) return Promise.resolve(window.Chartkick)
23
+ if (libraryPromise) return libraryPromise
24
+
25
+ libraryPromise = new Promise((resolve, reject) => {
26
+ const existing = document.querySelector(`script[src="${src}"]`)
27
+ const script = existing || document.createElement("script")
28
+
29
+ const settle = () => {
30
+ if (window.Chartkick) resolve(window.Chartkick)
31
+ else reject(new Error("[plutonium] the charts bundle loaded but defined no Chartkick"))
32
+ }
33
+
34
+ script.addEventListener("load", settle, { once: true })
35
+ script.addEventListener("error", () => {
36
+ libraryPromise = null
37
+ reject(new Error(`[plutonium] could not load the charts bundle from ${src}`))
38
+ }, { once: true })
39
+
40
+ if (!existing) {
41
+ script.src = src
42
+ script.async = true
43
+ script.dataset.turboTrack = "reload"
44
+ document.head.appendChild(script)
45
+ }
46
+ })
47
+
48
+ return libraryPromise
49
+ }
50
+
51
+ function cssVar(name) {
52
+ return getComputedStyle(document.documentElement).getPropertyValue(name).trim()
53
+ }
54
+
55
+ function themeColors() {
56
+ const colors = []
57
+ for (let i = 1; i <= PALETTE_SIZE; i++) {
58
+ const color = cssVar(`--pu-chart-${i}`)
59
+ if (color) colors.push(color)
60
+ }
61
+ return colors
62
+ }
63
+
64
+ function isPlainObject(value) {
65
+ return value !== null && typeof value === "object" && !Array.isArray(value)
66
+ }
67
+
68
+ function deepMerge(base, extra) {
69
+ const out = { ...base }
70
+ for (const [key, value] of Object.entries(extra || {})) {
71
+ out[key] = isPlainObject(value) && isPlainObject(out[key]) ? deepMerge(out[key], value) : value
72
+ }
73
+ return out
74
+ }
75
+
76
+ export default class extends Controller {
77
+ static values = {
78
+ type: { type: String, default: "LineChart" },
79
+ data: String,
80
+ options: Object,
81
+ script: String
82
+ }
83
+
84
+ connect() {
85
+ this.render = this.render.bind(this)
86
+ this.observer = new MutationObserver((mutations) => {
87
+ if (mutations.some((m) => m.attributeName === "class")) this.render()
88
+ })
89
+ this.observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] })
90
+
91
+ this.element.addEventListener("turbo:morph-element", this.render)
92
+
93
+ loadLibrary(this.scriptValue)
94
+ .then(() => this.render())
95
+ .catch((error) => {
96
+ console.error(error)
97
+ this.element.textContent = t("plutonium.js.charts.failed")
98
+ })
99
+ }
100
+
101
+ disconnect() {
102
+ this.observer?.disconnect()
103
+ this.element.removeEventListener("turbo:morph-element", this.render)
104
+ this.#destroy()
105
+ }
106
+
107
+ dataValueChanged() { this.render() }
108
+ optionsValueChanged() { this.render() }
109
+
110
+ render() {
111
+ const Chartkick = window.Chartkick
112
+ if (!Chartkick || !this.element.isConnected) return
113
+
114
+ const Klass = Chartkick[this.typeValue]
115
+ if (!Klass) {
116
+ console.error(`[plutonium] unknown chart type ${this.typeValue}`)
117
+ return
118
+ }
119
+
120
+ this.#destroy()
121
+ this.element.replaceChildren()
122
+ this.chart = new Klass(this.element, this.#data(), this.#options())
123
+ }
124
+
125
+ #destroy() {
126
+ if (this.chart) {
127
+ this.chart.destroy()
128
+ this.chart = null
129
+ }
130
+ }
131
+
132
+ #data() {
133
+ try {
134
+ return JSON.parse(this.dataValue || "[]")
135
+ } catch (error) {
136
+ console.error("[plutonium] chart data is not valid JSON", error)
137
+ return []
138
+ }
139
+ }
140
+
141
+ // Plutonium's defaults, with the card's own options merged over them.
142
+ // Axis styling only applies to cartesian charts: handing a pie chart
143
+ // `scales` makes Chart.js draw axes around it.
144
+ #options() {
145
+ const text = cssVar("--pu-text-muted")
146
+ const grid = cssVar("--pu-border")
147
+ const surface = cssVar("--pu-card-bg")
148
+ const library = { color: text }
149
+
150
+ if (this.typeValue === "PieChart") {
151
+ library.elements = { arc: { borderColor: surface } }
152
+ } else {
153
+ library.scales = {
154
+ x: { ticks: { color: text }, grid: { color: grid }, border: { color: grid } },
155
+ y: { ticks: { color: text }, grid: { color: grid }, border: { color: grid } }
156
+ }
157
+ }
158
+
159
+ const defaults = { colors: themeColors(), empty: t("plutonium.js.charts.empty"), library }
160
+ return deepMerge(defaults, this.optionsValue)
161
+ }
162
+ }
@@ -1,4 +1,5 @@
1
1
  import { Controller } from "@hotwired/stimulus"
2
+ import { t } from "../i18n.js"
2
3
 
3
4
  export default class extends Controller {
4
5
  static targets = ["source"]
@@ -9,7 +10,7 @@ export default class extends Controller {
9
10
  const originalText = button.textContent
10
11
 
11
12
  navigator.clipboard.writeText(text).then(() => {
12
- button.textContent = "Copied!"
13
+ button.textContent = t("plutonium.js.clipboard.copied")
13
14
  setTimeout(() => {
14
15
  button.textContent = originalText
15
16
  }, 2000)
@@ -17,7 +18,7 @@ export default class extends Controller {
17
18
  // Fallback for browsers that don't support clipboard API
18
19
  console.warn("Clipboard API failed, using fallback:", err)
19
20
  this.fallbackCopy(text)
20
- button.textContent = "Copied!"
21
+ button.textContent = t("plutonium.js.clipboard.copied")
21
22
  setTimeout(() => {
22
23
  button.textContent = originalText
23
24
  }, 2000)
@@ -1,4 +1,5 @@
1
- import { Controller } from "@hotwired/stimulus";
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import { t } from "../i18n.js";
2
3
 
3
4
  // Connects to data-controller="dirty-form-guard"
4
5
  // Prompts before discarding a form's unsaved changes. Two guard surfaces,
@@ -220,7 +221,7 @@ export default class extends Controller {
220
221
 
221
222
  const message =
222
223
  trigger.getAttribute("data-dirty-form-guard-leave") ||
223
- "You have unsaved changes that will be lost. Continue?";
224
+ t("plutonium.js.dirty_form_guard.unsaved_changes");
224
225
  const confirmed = await this.#confirm(message);
225
226
  if (!confirmed) return;
226
227
 
@@ -339,7 +340,7 @@ export default class extends Controller {
339
340
  requestAnimationFrame(() => {
340
341
  requestAnimationFrame(() => d.setAttribute("data-open", ""));
341
342
  });
342
- } else if (window.confirm("Discard your changes?")) {
343
+ } else if (window.confirm(t("plutonium.js.dirty_form_guard.discard_changes"))) {
343
344
  this.forceClose = true;
344
345
  this.dialog.dispatchEvent(new CustomEvent("modal:request-close"));
345
346
  }
@@ -3,6 +3,9 @@ import DOMPurify from 'dompurify';
3
3
  import { marked } from 'marked';
4
4
 
5
5
  // Connects to data-controller="easymde"
6
+ //
7
+ // Not translated: EasyMDE has no locale option (its toolbar titles are
8
+ // per-button config), so there is no clean plutonium.js.libraries hook.
6
9
  export default class extends Controller {
7
10
  static targets = ["textarea"]
8
11
 
@@ -59,10 +59,20 @@ export default class extends Controller {
59
59
  this._previousBodyOverflow = null
60
60
  }
61
61
 
62
- // Reset every input under this controller's scope, then submit so the
63
- // table reflects the cleared filters immediately.
62
+ // Reset every input in the FILTER form, then submit it so the collection
63
+ // reflects the cleared filters immediately.
64
+ //
65
+ // Scoped to #filter-form, NOT the whole controller element: the toolbar's
66
+ // search box is a separate <form> that lives in the same scope but before
67
+ // this one. Submitting that bare form (or clearing its inputs) would drop the
68
+ // view / sort / scope state the filter form preserves via hidden inputs —
69
+ // which on the kanban board bounced the user back to the table (the dropped
70
+ // ?view=kanban fell through to the default view).
64
71
  clear() {
65
- this.element.querySelectorAll("input, select, textarea").forEach(input => {
72
+ const form = this.element.querySelector("#filter-form")
73
+ if (!form) return
74
+
75
+ form.querySelectorAll("input, select, textarea").forEach(input => {
66
76
  if (input.type === "checkbox" || input.type === "radio") {
67
77
  input.checked = false
68
78
  } else if (input.tagName === "SELECT") {
@@ -74,13 +84,12 @@ export default class extends Controller {
74
84
  }
75
85
  })
76
86
 
77
- this.element.querySelectorAll('[data-controller="flatpickr"]').forEach(input => {
87
+ form.querySelectorAll('[data-controller="flatpickr"]').forEach(input => {
78
88
  const controller = this.application.getControllerForElementAndIdentifier(input, "flatpickr")
79
89
  if (controller?.picker) controller.picker.clear()
80
90
  })
81
91
 
82
- const form = this.element.querySelector("form")
83
- if (form) form.requestSubmit()
92
+ form.requestSubmit()
84
93
  }
85
94
 
86
95
  get isOpen() {
@@ -1,4 +1,5 @@
1
1
  import { Controller } from "@hotwired/stimulus";
2
+ import { lookup } from "../i18n.js";
2
3
 
3
4
  // Connects to data-controller="flatpickr"
4
5
  export default class extends Controller {
@@ -44,6 +45,13 @@ export default class extends Controller {
44
45
  #buildOptions() {
45
46
  let options = { altInput: true };
46
47
 
48
+ // Optional locale passthrough: plutonium.js.libraries.flatpickr.locale is
49
+ // the flatpickr locale key (e.g. "de"). Loading the matching l10n bundle
50
+ // (flatpickr/dist/l10n/de.js) is the host app's job; without it flatpickr
51
+ // falls back to English.
52
+ const locale = lookup("plutonium.js.libraries.flatpickr.locale");
53
+ if (typeof locale === "string") options.locale = locale;
54
+
47
55
  if (this.element.attributes.type.value == "datetime-local") {
48
56
  options.enableTime = true;
49
57
  } else if (this.element.attributes.type.value == "time") {
@@ -0,0 +1,59 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ // Connects to data-controller="frame-refresh"
4
+ //
5
+ // Reloads a `<turbo-frame src>` every `interval` seconds while the tab is
6
+ // visible, so a dashboard card declared with `refresh:` keeps itself current
7
+ // without a page reload. Pauses when the document is hidden and resumes with
8
+ // an immediate reload when it becomes visible again after a missed tick.
9
+ export default class extends Controller {
10
+ static values = { interval: Number }
11
+
12
+ connect() {
13
+ this.tick = this.tick.bind(this)
14
+ this.onVisibility = this.onVisibility.bind(this)
15
+ document.addEventListener("visibilitychange", this.onVisibility)
16
+ this.#start()
17
+ }
18
+
19
+ disconnect() {
20
+ document.removeEventListener("visibilitychange", this.onVisibility)
21
+ this.#stop()
22
+ }
23
+
24
+ intervalValueChanged() {
25
+ this.#stop()
26
+ this.#start()
27
+ }
28
+
29
+ tick() {
30
+ if (document.hidden) {
31
+ this.missed = true
32
+ return
33
+ }
34
+ this.reload()
35
+ }
36
+
37
+ reload() {
38
+ this.missed = false
39
+ if (typeof this.element.reload === "function" && this.element.getAttribute("src")) {
40
+ this.element.reload()
41
+ }
42
+ }
43
+
44
+ onVisibility() {
45
+ if (!document.hidden && this.missed) this.reload()
46
+ }
47
+
48
+ #start() {
49
+ if (!(this.intervalValue > 0)) return
50
+ this.timer = setInterval(this.tick, this.intervalValue * 1000)
51
+ }
52
+
53
+ #stop() {
54
+ if (this.timer) {
55
+ clearInterval(this.timer)
56
+ this.timer = null
57
+ }
58
+ }
59
+ }
@@ -1,4 +1,5 @@
1
1
  import { Controller } from "@hotwired/stimulus"
2
+ import { t } from "../i18n.js"
2
3
 
3
4
  // Connects to data-controller="intl-tel-input"
4
5
  export default class extends Controller {
@@ -52,7 +53,11 @@ export default class extends Controller {
52
53
  #buildOptions() {
53
54
  // Defaults first; the definition-supplied `options` value (e.g.
54
55
  // { initialCountry: "gh", strictMode: false }) is spread last so it wins.
56
+ // Optional locale passthrough: plutonium.js.libraries.intl_tel_input is
57
+ // the intl-tel-input `i18n` option (an object of strings).
58
+ const i18n = t("plutonium.js.libraries.intl_tel_input")
55
59
  return {
60
+ ...(i18n && typeof i18n === "object" ? { i18n } : {}),
56
61
  strictMode: true,
57
62
  hiddenInput: () => ({ phone: this.inputTarget.attributes.name.value }),
58
63
  loadUtilsOnInit: "https://cdn.jsdelivr.net/npm/intl-tel-input@24.8.1/build/js/utils.js",