plutonium 0.64.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 (278) 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/CHANGELOG.md +59 -0
  8. data/CLAUDE.md +282 -210
  9. data/SECURITY.md +1 -1
  10. data/app/assets/plutonium-charts.js +20604 -0
  11. data/app/assets/plutonium-charts.js.map +7 -0
  12. data/app/assets/plutonium-charts.min.js +41 -0
  13. data/app/assets/plutonium-charts.min.js.map +7 -0
  14. data/app/assets/plutonium.css +1 -1
  15. data/app/assets/plutonium.js +794 -629
  16. data/app/assets/plutonium.js.map +4 -4
  17. data/app/assets/plutonium.min.js +48 -48
  18. data/app/assets/plutonium.min.js.map +4 -4
  19. data/app/views/plutonium/_flash_alerts.html.erb +2 -2
  20. data/app/views/plutonium/_resource_header.html.erb +2 -2
  21. data/app/views/plutonium/_resource_sidebar.html.erb +11 -2
  22. data/app/views/plutonium/_toast.html.erb +2 -2
  23. data/app/views/rodauth/_login_form.html.erb +1 -1
  24. data/app/views/rodauth/_password_visibility.html.erb +1 -1
  25. data/app/views/rodauth/add_recovery_codes.html.erb +2 -2
  26. data/app/views/rodauth/change_login.html.erb +2 -2
  27. data/app/views/rodauth/create_account.html.erb +4 -4
  28. data/app/views/rodauth/reset_password_request.html.erb +1 -1
  29. data/app/views/rodauth/verify_account_resend.html.erb +1 -1
  30. data/app/views/rodauth/webauthn_remove.html.erb +1 -1
  31. data/config/brakeman.ignore +4 -4
  32. data/config/initializers/rabl.rb +0 -40
  33. data/config/locales/en/api_client.yml +22 -0
  34. data/config/locales/en/async.yml +34 -0
  35. data/config/locales/en/dashboard.yml +25 -0
  36. data/config/locales/en/invites.yml +37 -0
  37. data/config/locales/en/js.yml +40 -0
  38. data/config/locales/en/kanban.yml +28 -0
  39. data/config/locales/en/plutonium.yml +26 -0
  40. data/config/locales/en/positioning.yml +14 -0
  41. data/config/locales/en/profile.yml +29 -0
  42. data/config/locales/en/resource.yml +60 -0
  43. data/config/locales/en/ui.yml +93 -0
  44. data/config/locales/en/wizard.yml +46 -0
  45. data/docs/.vitepress/config.ts +34 -1
  46. data/docs/.vitepress/theme/components/HomeHero.vue +1 -1
  47. data/docs/.vitepress/theme/components/HomeInTheBox.vue +14 -0
  48. data/docs/.vitepress/theme/custom.css +34 -377
  49. data/docs/blog/introducing-plutonium-dashboards.md +175 -0
  50. data/docs/blog/introducing-plutonium-i18n.md +91 -0
  51. data/docs/blog/introducing-plutonium.md +4 -5
  52. data/docs/blog/whats-new-async-kanban-wizards.md +1 -1
  53. data/docs/guides/dashboards.md +300 -0
  54. data/docs/guides/index.md +1 -0
  55. data/docs/guides/kanban.md +2 -2
  56. data/docs/public/images/blog/dashboards-card-error.png +0 -0
  57. data/docs/public/images/blog/dashboards-loading.png +0 -0
  58. data/docs/public/images/blog/dashboards-overview.png +0 -0
  59. data/docs/public/images/blog/dashboards-sidebar.png +0 -0
  60. data/docs/public/images/blog/i18n-es-admin.png +0 -0
  61. data/docs/public/images/guides/dashboard-card-error.png +0 -0
  62. data/docs/public/images/guides/dashboard-loading.png +0 -0
  63. data/docs/public/images/guides/dashboard-overview.png +0 -0
  64. data/docs/public/images/guides/dashboard-sidebar.png +0 -0
  65. data/docs/reference/app/generators.md +2 -2
  66. data/docs/reference/app/portals.md +2 -0
  67. data/docs/reference/configuration.md +84 -27
  68. data/docs/reference/dashboard/dsl.md +123 -0
  69. data/docs/reference/dashboard/index.md +35 -0
  70. data/docs/reference/dashboard/registration.md +76 -0
  71. data/docs/reference/i18n.md +239 -0
  72. data/docs/reference/index.md +10 -1
  73. data/docs/reference/kanban/dsl.md +1 -1
  74. data/docs/reference/kanban/positioning.md +2 -2
  75. data/docs/reference/resource/actions.md +1 -1
  76. data/docs/reference/resource/definition.md +96 -18
  77. data/docs/reference/resource/export.md +5 -1
  78. data/docs/reference/resource/index.md +2 -0
  79. data/docs/reference/{positioning.md → resource/positioning.md} +2 -1
  80. data/docs/reference/resource/query.md +2 -0
  81. data/docs/reference/ui/assets.md +8 -0
  82. data/docs/reference/ui/displays.md +5 -8
  83. data/docs/reference/ui/index.md +1 -1
  84. data/docs/reference/wizard/dsl.md +2 -2
  85. data/esbuild.config.js +5 -2
  86. data/gemfiles/rails_7.gemfile.lock +2 -2
  87. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  88. data/lib/active_model/validations/array_validator.rb +1 -1
  89. data/lib/active_model/validations/attached_validator.rb +1 -1
  90. data/lib/active_model/validations/url_validator.rb +2 -2
  91. data/lib/generators/pu/core/assets/assets_generator.rb +36 -21
  92. data/lib/generators/pu/core/assets/templates/postcss.config.js +1 -1
  93. data/lib/generators/pu/core/update/update_generator.rb +3 -2
  94. data/lib/generators/pu/dashboard/dashboard_generator.rb +96 -0
  95. data/lib/generators/pu/dashboard/templates/dashboard.rb.tt +25 -0
  96. data/lib/generators/pu/docker/install/install_generator.rb +58 -0
  97. data/lib/generators/pu/docker/install/templates/Dockerfile.dev.tt +18 -3
  98. data/lib/generators/pu/docker/install/templates/Dockerfile.tt +20 -5
  99. data/lib/generators/pu/docker/install/templates/{docker-compose.yml → docker-compose.yml.tt} +1 -1
  100. data/lib/generators/pu/gem/dotenv/dotenv_generator.rb +1 -1
  101. data/lib/generators/pu/invites/invitable_generator.rb +18 -1
  102. data/lib/generators/pu/lib/plutonium_generators/concerns/actions.rb +0 -95
  103. data/lib/generators/pu/lib/plutonium_generators/concerns/js_package_manager.rb +146 -0
  104. data/lib/generators/pu/lib/plutonium_generators/generator.rb +1 -17
  105. data/lib/generators/pu/pkg/package/package_generator.rb +1 -0
  106. data/lib/generators/pu/pkg/package/templates/config/locales/en.yml.tt +28 -0
  107. data/lib/generators/pu/pkg/portal/portal_generator.rb +1 -0
  108. data/lib/generators/pu/pkg/portal/templates/config/locales/en.yml.tt +30 -0
  109. data/lib/generators/pu/res/scaffold/scaffold_generator.rb +0 -8
  110. data/lib/generators/pu/rodauth/concerns/feature_selector.rb +0 -18
  111. data/lib/plutonium/action/base.rb +27 -3
  112. data/lib/plutonium/action/interactive.rb +17 -4
  113. data/lib/plutonium/api_client/concerns/create_api_client.rb +12 -11
  114. data/lib/plutonium/api_client/concerns/disable_api_client.rb +3 -3
  115. data/lib/plutonium/auth/sequel_adapter.rb +1 -1
  116. data/lib/plutonium/configuration.rb +5 -0
  117. data/lib/plutonium/core/controller.rb +25 -1
  118. data/lib/plutonium/core/controllers/authorizable.rb +1 -1
  119. data/lib/plutonium/dashboard/base.rb +114 -0
  120. data/lib/plutonium/dashboard/card.rb +200 -0
  121. data/lib/plutonium/dashboard/controller.rb +101 -0
  122. data/lib/plutonium/dashboard/dsl.rb +104 -0
  123. data/lib/plutonium/dashboard/register.rb +40 -0
  124. data/lib/plutonium/dashboard/route_resolution.rb +30 -0
  125. data/lib/plutonium/dashboard.rb +42 -0
  126. data/lib/plutonium/definition/actions.rb +6 -1
  127. data/lib/plutonium/definition/base.rb +41 -3
  128. data/lib/plutonium/definition/index_views.rb +1 -1
  129. data/lib/plutonium/definition/positioning.rb +1 -1
  130. data/lib/plutonium/definition/presentable.rb +2 -2
  131. data/lib/plutonium/helpers/assets_helper.rb +15 -2
  132. data/lib/plutonium/helpers/display_helper.rb +10 -2
  133. data/lib/plutonium/interaction/async/context.rb +6 -8
  134. data/lib/plutonium/interaction/async/executor.rb +6 -7
  135. data/lib/plutonium/interaction/async/run.rb +2 -2
  136. data/lib/plutonium/interaction/async/run_definition.rb +1 -1
  137. data/lib/plutonium/interaction/base.rb +2 -0
  138. data/lib/plutonium/interaction/concerns/dispatchable.rb +15 -3
  139. data/lib/plutonium/invites/concerns/cancel_invite.rb +3 -3
  140. data/lib/plutonium/invites/concerns/invite_token.rb +25 -7
  141. data/lib/plutonium/invites/concerns/invite_user.rb +10 -5
  142. data/lib/plutonium/invites/concerns/resend_invite.rb +4 -4
  143. data/lib/plutonium/invites/controller.rb +11 -11
  144. data/lib/plutonium/kanban/column.rb +13 -3
  145. data/lib/plutonium/kanban/dsl.rb +7 -4
  146. data/lib/plutonium/models/has_cents.rb +1 -1
  147. data/lib/plutonium/profile/security_section.rb +10 -18
  148. data/lib/plutonium/query/filter.rb +10 -1
  149. data/lib/plutonium/query/filters/association.rb +27 -2
  150. data/lib/plutonium/query/filters/boolean.rb +4 -4
  151. data/lib/plutonium/query/filters/date.rb +4 -17
  152. data/lib/plutonium/query/filters/date_range.rb +2 -2
  153. data/lib/plutonium/query/filters/select.rb +10 -1
  154. data/lib/plutonium/query/filters/text.rb +9 -12
  155. data/lib/plutonium/railtie.rb +17 -0
  156. data/lib/plutonium/resource/controller.rb +10 -5
  157. data/lib/plutonium/resource/controllers/crud_actions.rb +17 -20
  158. data/lib/plutonium/resource/controllers/export_csv.rb +19 -9
  159. data/lib/plutonium/resource/controllers/kanban_actions.rb +16 -16
  160. data/lib/plutonium/resource/controllers/position_actions.rb +5 -5
  161. data/lib/plutonium/resource/controllers/typeahead.rb +24 -1
  162. data/lib/plutonium/resource/query_object.rb +30 -3
  163. data/lib/plutonium/routing/dashboard_registration.rb +137 -0
  164. data/lib/plutonium/routing/resource_registration.rb +5 -0
  165. data/lib/plutonium/routing/route_set_extensions.rb +2 -1
  166. data/lib/plutonium/translation.rb +227 -0
  167. data/lib/plutonium/ui/actions_dropdown.rb +2 -2
  168. data/lib/plutonium/ui/breadcrumbs.rb +3 -3
  169. data/lib/plutonium/ui/color_mode_selector.rb +1 -1
  170. data/lib/plutonium/ui/component/behaviour.rb +46 -0
  171. data/lib/plutonium/ui/component/methods.rb +4 -0
  172. data/lib/plutonium/ui/dashboard/board.rb +80 -0
  173. data/lib/plutonium/ui/dashboard/card.rb +127 -0
  174. data/lib/plutonium/ui/dashboard/chart.rb +43 -0
  175. data/lib/plutonium/ui/dashboard/custom.rb +32 -0
  176. data/lib/plutonium/ui/dashboard/frame.rb +25 -0
  177. data/lib/plutonium/ui/dashboard/metric.rb +137 -0
  178. data/lib/plutonium/ui/dashboard/skeleton.rb +40 -0
  179. data/lib/plutonium/ui/display/components/attachment.rb +1 -1
  180. data/lib/plutonium/ui/display/components/badge.rb +7 -1
  181. data/lib/plutonium/ui/display/components/boolean.rb +2 -2
  182. data/lib/plutonium/ui/display/resource.rb +23 -16
  183. data/lib/plutonium/ui/export_button.rb +3 -3
  184. data/lib/plutonium/ui/form/components/key_value_store.rb +5 -5
  185. data/lib/plutonium/ui/form/components/password.rb +2 -2
  186. data/lib/plutonium/ui/form/components/resource_select.rb +27 -0
  187. data/lib/plutonium/ui/form/components/secure_association.rb +2 -0
  188. data/lib/plutonium/ui/form/components/uppy.rb +11 -3
  189. data/lib/plutonium/ui/form/concerns/renders_nested_resource_fields.rb +1 -1
  190. data/lib/plutonium/ui/form/concerns/renders_repeater_row_controls.rb +5 -4
  191. data/lib/plutonium/ui/form/concerns/renders_structured_inputs.rb +1 -1
  192. data/lib/plutonium/ui/form/query.rb +8 -8
  193. data/lib/plutonium/ui/form/resource.rb +54 -26
  194. data/lib/plutonium/ui/frame_navigator_panel.rb +4 -4
  195. data/lib/plutonium/ui/grid/card.rb +3 -3
  196. data/lib/plutonium/ui/grid/resource.rb +3 -3
  197. data/lib/plutonium/ui/interaction/async/run_progress.rb +6 -6
  198. data/lib/plutonium/ui/interaction/async/running_banner.rb +2 -2
  199. data/lib/plutonium/ui/kanban/column.rb +6 -6
  200. data/lib/plutonium/ui/layout/base.rb +19 -1
  201. data/lib/plutonium/ui/layout/header.rb +1 -1
  202. data/lib/plutonium/ui/layout/icon_rail.rb +4 -4
  203. data/lib/plutonium/ui/layout/rodauth_layout.rb +1 -1
  204. data/lib/plutonium/ui/layout/sidebar.rb +1 -1
  205. data/lib/plutonium/ui/layout/topbar.rb +2 -2
  206. data/lib/plutonium/ui/modal/base.rb +4 -4
  207. data/lib/plutonium/ui/nav_grid_menu.rb +1 -1
  208. data/lib/plutonium/ui/nav_user.rb +2 -2
  209. data/lib/plutonium/ui/page/base.rb +10 -2
  210. data/lib/plutonium/ui/page/dashboard.rb +54 -0
  211. data/lib/plutonium/ui/page/edit.rb +3 -3
  212. data/lib/plutonium/ui/page/interactive_action.rb +2 -2
  213. data/lib/plutonium/ui/page/new.rb +3 -3
  214. data/lib/plutonium/ui/page/show.rb +2 -2
  215. data/lib/plutonium/ui/page/wizard.rb +22 -16
  216. data/lib/plutonium/ui/page/wizard_chooser.rb +10 -10
  217. data/lib/plutonium/ui/page/wizard_completed.rb +2 -2
  218. data/lib/plutonium/ui/skeleton_table.rb +1 -1
  219. data/lib/plutonium/ui/table/base.rb +2 -2
  220. data/lib/plutonium/ui/table/components/attachment.rb +1 -1
  221. data/lib/plutonium/ui/table/components/bulk_actions_toolbar.rb +19 -3
  222. data/lib/plutonium/ui/table/components/drag_handle.rb +3 -3
  223. data/lib/plutonium/ui/table/components/filter_form.rb +11 -10
  224. data/lib/plutonium/ui/table/components/filter_pills.rb +4 -4
  225. data/lib/plutonium/ui/table/components/pagy_info.rb +61 -17
  226. data/lib/plutonium/ui/table/components/pagy_pagination.rb +9 -5
  227. data/lib/plutonium/ui/table/components/row_actions_dropdown.rb +1 -1
  228. data/lib/plutonium/ui/table/components/scopes_bar.rb +6 -2
  229. data/lib/plutonium/ui/table/components/scopes_pills.rb +3 -3
  230. data/lib/plutonium/ui/table/components/selection_column.rb +1 -1
  231. data/lib/plutonium/ui/table/components/toolbar.rb +2 -2
  232. data/lib/plutonium/ui/table/components/view_switcher.rb +12 -8
  233. data/lib/plutonium/ui/table/resource.rb +53 -17
  234. data/lib/plutonium/ui/wizard/review.rb +7 -7
  235. data/lib/plutonium/ui/wizard/stepper.rb +1 -1
  236. data/lib/plutonium/ui/wizard/summary_display.rb +6 -0
  237. data/lib/plutonium/version.rb +1 -1
  238. data/lib/plutonium/wizard/base.rb +2 -0
  239. data/lib/plutonium/wizard/driving.rb +8 -2
  240. data/lib/plutonium/wizard/dsl.rb +4 -2
  241. data/lib/plutonium/wizard/field_importer.rb +1 -8
  242. data/lib/plutonium/wizard/review_step.rb +8 -1
  243. data/lib/plutonium/wizard/step.rb +14 -3
  244. data/package.json +4 -2
  245. data/plutonium.gemspec +4 -1
  246. data/postcss-gem-import.cjs +34 -0
  247. data/postcss-gem-import.js +3 -28
  248. data/src/css/components.css +91 -0
  249. data/src/css/slim_select.css +0 -38
  250. data/src/css/tokens.css +24 -0
  251. data/src/js/controllers/attachment_input_controller.js +15 -6
  252. data/src/js/controllers/chart_controller.js +162 -0
  253. data/src/js/controllers/clipboard_controller.js +3 -2
  254. data/src/js/controllers/dirty_form_guard_controller.js +4 -3
  255. data/src/js/controllers/easymde_controller.js +3 -0
  256. data/src/js/controllers/filter_panel_controller.js +15 -6
  257. data/src/js/controllers/flatpickr_controller.js +8 -0
  258. data/src/js/controllers/frame_refresh_controller.js +59 -0
  259. data/src/js/controllers/intl_tel_input_controller.js +5 -0
  260. data/src/js/controllers/register_controllers.js +4 -0
  261. data/src/js/controllers/slim_select_controller.js +79 -176
  262. data/src/js/core.js +9 -1
  263. data/src/js/i18n.js +69 -0
  264. data/src/js/plutonium-charts.js +15 -0
  265. data/src/js/turbo/turbo_confirm.js +5 -3
  266. metadata +64 -16
  267. data/.yarnrc.yml +0 -8
  268. data/config/initializers/hotwire_turbo_monkey_patches.rb +0 -12
  269. data/lib/generators/pu/field/input/input_generator.rb +0 -32
  270. data/lib/generators/pu/field/input/templates/input.rb.tt +0 -15
  271. data/lib/generators/pu/lib/plutonium_generators/concerns/config.rb +0 -39
  272. data/lib/generators/pu/lib/plutonium_generators/concerns/serializer.rb +0 -39
  273. data/lib/generators/pu/lib/plutonium_generators/installer.rb +0 -205
  274. data/lib/generators/pu/res/scaffold/templates/presenter.rb.tt +0 -13
  275. data/lib/generators/pu/res/scaffold/templates/query_object.rb.tt +0 -17
  276. data/public/plutonium-assets/application.js +0 -31419
  277. data/public/plutonium-assets/plutonium.ico +0 -0
  278. data/yarn.lock +0 -6858
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",
@@ -41,6 +41,8 @@ import PositionedController from "./positioned_controller.js"
41
41
  import CurrencyInputController from "./currency_input_controller.js"
42
42
  import BreadcrumbsController from "./breadcrumbs_controller.js"
43
43
  import RunProgressController from "./run_progress_controller.js"
44
+ import ChartController from "./chart_controller.js"
45
+ import FrameRefreshController from "./frame_refresh_controller.js"
44
46
 
45
47
  export default function (application) {
46
48
  // Register controllers here
@@ -86,4 +88,6 @@ export default function (application) {
86
88
  application.register("currency-input", CurrencyInputController)
87
89
  application.register("breadcrumbs", BreadcrumbsController)
88
90
  application.register("run-progress", RunProgressController)
91
+ application.register("chart", ChartController)
92
+ application.register("frame-refresh", FrameRefreshController)
89
93
  }