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
@@ -12,10 +12,12 @@ module Plutonium
12
12
  # through. The Stimulus controller sets the cookie on click,
13
13
  # then reloads.
14
14
  class ViewSwitcher < Plutonium::UI::Component::Base
15
+ # Labels are translation keys, resolved at render time so the
16
+ # switcher follows the request's locale.
15
17
  SEGMENT_LABELS = {
16
- table: {label: "Table", icon: Phlex::TablerIcons::Table},
17
- grid: {label: "Grid", icon: Phlex::TablerIcons::LayoutGrid},
18
- kanban: {label: "Board", icon: Phlex::TablerIcons::LayoutKanban}
18
+ table: {label: "plutonium.ui.table.views.table", icon: Phlex::TablerIcons::Table},
19
+ grid: {label: "plutonium.ui.table.views.grid", icon: Phlex::TablerIcons::LayoutGrid},
20
+ kanban: {label: "plutonium.ui.table.views.kanban", icon: Phlex::TablerIcons::LayoutKanban}
19
21
  }.freeze
20
22
 
21
23
  def initialize(views:, current:, cookie_name:, cookie_path: "/")
@@ -32,7 +34,7 @@ module Plutonium
32
34
  def view_template
33
35
  div(
34
36
  role: "tablist",
35
- aria: {label: "View"},
37
+ aria: {label: t("plutonium.ui.table.view")},
36
38
  class: "inline-flex h-8 rounded-md border border-[var(--pu-border)] bg-[var(--pu-surface)] overflow-hidden",
37
39
  data: {
38
40
  controller: "view-switcher",
@@ -49,7 +51,9 @@ module Plutonium
49
51
  private
50
52
 
51
53
  def render_segment(key, last:)
52
- meta = SEGMENT_LABELS.fetch(key) { {label: key.to_s.titleize, icon: Phlex::TablerIcons::LayoutGrid} }
54
+ meta = SEGMENT_LABELS[key]
55
+ label = meta ? t(meta[:label]) : key.to_s.titleize
56
+ icon = meta ? meta[:icon] : Phlex::TablerIcons::LayoutGrid
53
57
  active = key == @current
54
58
 
55
59
  classes = ["px-2.5 inline-flex items-center gap-1.5 text-sm transition-colors"]
@@ -64,15 +68,15 @@ module Plutonium
64
68
  type: "button",
65
69
  role: "tab",
66
70
  class: classes.join(" "),
67
- title: meta[:label],
71
+ title: label,
68
72
  aria: {selected: active.to_s},
69
73
  data: {
70
74
  action: "click->view-switcher#select",
71
75
  view_switcher_view_param: key.to_s
72
76
  }
73
77
  ) do
74
- render meta[:icon].new(class: "w-4 h-4 shrink-0")
75
- span { meta[:label] }
78
+ render icon.new(class: "w-4 h-4 shrink-0")
79
+ span { label }
76
80
  end
77
81
  end
78
82
  end
@@ -84,7 +84,7 @@ module Plutonium
84
84
  end
85
85
 
86
86
  def render_empty_card
87
- EmptyCard("No #{resource_name_plural(resource_class).downcase} available") {
87
+ EmptyCard(t("plutonium.ui.empty_state.none_available", resources: resource_name_plural(resource_class).downcase)) {
88
88
  action = resource_definition.defined_actions[:new]
89
89
  if action&.permitted_by?(current_policy)
90
90
  url = route_options_to_url(action.route_options, resource_class)
@@ -121,19 +121,33 @@ module Plutonium
121
121
  column_definition = resource_definition.defined_columns[name] || {}
122
122
  column_options = column_definition[:options] || {}
123
123
 
124
- # Check for conditional rendering
125
- condition = column_options[:condition]
124
+ field_options = resolve_field_level_procs(field_options)
125
+ display_options = resolve_field_level_procs(display_options)
126
+ column_options = resolve_field_level_procs(column_options)
127
+
128
+ # A `column` condition or a surface-neutral `field` condition hides
129
+ # the column. A `display` condition stays show-only: it may reference
130
+ # `object`, and the table has no single record to evaluate it against.
131
+ condition = column_options[:condition] || field_options[:condition]
126
132
  conditionally_hidden = condition && !instance_exec(&condition)
127
133
  next if conditionally_hidden
128
134
 
129
- tag = column_options[:as] || display_definition[:as] || field_options[:as]
135
+ # A column that says anything about RENDERING (`as:`, a component
136
+ # attribute, or a block) renders alone: its own `as:` and attributes,
137
+ # no display inheritance. A column that only touches the header
138
+ # (align/label/condition) layers on whatever the display renders, so
139
+ # `display :x, as: :badge` + `column :x, align: :end` keeps the badge.
140
+ # Only attributes are inherited, never the display's block.
141
+ column_render_options = column_options.except(:wrapper, :condition, *COLUMN_FIELD_LEVEL_KEYS)
142
+ renders_alone = column_definition[:block] || column_render_options.present?
143
+ source_options = renders_alone ? column_options : display_options
144
+
145
+ tag = source_options[:as] || field_options[:as]
130
146
 
131
- # Extract field-level options from display_options and column_options
132
- # These are Phlexi field options that should NOT be passed to the tag builder
133
- field_level_keys = [:label, :description, :placeholder]
134
- display_tag_attributes = display_options.except(:wrapper, :as, :condition, *field_level_keys)
135
- column_tag_attributes = column_options.except(:wrapper, :as, :align, :condition, *field_level_keys)
136
- tag_attributes = display_tag_attributes.merge(column_tag_attributes)
147
+ # Strip the field-level help keys and the header keys: they configure
148
+ # the field or the column, not the tag, and would otherwise leak as an
149
+ # HTML attribute.
150
+ tag_attributes = source_options.except(:wrapper, :as, :condition, *FIELD_LEVEL_KEYS, *COLUMN_FIELD_LEVEL_KEYS)
137
151
 
138
152
  # A `formatter:` produces the cell string itself, so render it
139
153
  # through the formatted-value component regardless of the column's
@@ -142,9 +156,17 @@ module Plutonium
142
156
  tag = :formatted_value if tag_attributes.key?(:formatter)
143
157
 
144
158
  tag_block = if column_definition[:block]
145
- # User-provided blocks receive the raw record for convenience
159
+ # User blocks receive the raw record and are instance_exec'd against
160
+ # this table page, exactly as display blocks are against the show
161
+ # page: `self` has resource_definition/current_user/helpers and the
162
+ # tag methods, so the body can emit markup directly. The cell is a
163
+ # zero-arity proc; Phlex `render`s it in the <td>, sharing this
164
+ # component's buffer.
146
165
  user_block = column_definition[:block]
147
- ->(wrapped_object, _key) { user_block.call(wrapped_object.unwrapped) }
166
+ ->(wrapped_object, _key) {
167
+ record = wrapped_object.unwrapped
168
+ -> { render_column_block_result(instance_exec(record, &user_block)) }
169
+ }
148
170
  else
149
171
  ->(wrapped_object, key) {
150
172
  f = wrapped_object.field(key)
@@ -160,9 +182,11 @@ module Plutonium
160
182
  tag_block = wrap_with_drag_handle(tag_block)
161
183
  end
162
184
 
163
- # For table columns, only extract column-level options (label and align)
164
- # Field-level options like description and placeholder don't make sense in table cells
165
- field_options = field_options.except(:condition).merge(**column_options.slice(:align, :label))
185
+ # Header options (label/align) come from field, then display, then
186
+ # column, the most specific winning. Field-level help keys like
187
+ # description and placeholder have no place in a table cell.
188
+ header_options = display_options.slice(*COLUMN_FIELD_LEVEL_KEYS).merge(column_options.slice(*COLUMN_FIELD_LEVEL_KEYS))
189
+ field_options = field_options.except(:condition).merge(header_options)
166
190
  table.column name,
167
191
  **field_options,
168
192
  sort_params: current_query_object.sort_params_for(name),
@@ -209,6 +233,18 @@ module Plutonium
209
233
  #
210
234
  # The per-record `reposition?` check, by contrast, HAS to run per row.
211
235
  # Rows that fail it render exactly as they did before, unwrapped.
236
+ # A block that emitted markup returns nil (a no-op render); a String,
237
+ # component or proc renders as Phlex normally would; anything else
238
+ # (a count, a date…) reads as text instead of raising.
239
+ def render_column_block_result(result)
240
+ case result
241
+ when nil, String, Phlex::SGML, Proc, Enumerable
242
+ render result
243
+ else
244
+ plain result.to_s
245
+ end
246
+ end
247
+
212
248
  def wrap_with_drag_handle(inner)
213
249
  sort_url = position_grip_sort_url
214
250
 
@@ -261,7 +297,7 @@ module Plutonium
261
297
  "data-[open]:translate-x-0 " \
262
298
  "flex flex-col",
263
299
  role: "dialog",
264
- aria: {label: "Filters", hidden: "true", modal: "true"},
300
+ aria: {label: t("plutonium.ui.table.filters"), hidden: "true", modal: "true"},
265
301
  data: {filter_panel_target: "panel"}
266
302
  ) do
267
303
  render Plutonium::UI::Table::Components::FilterForm.new(
@@ -279,7 +315,7 @@ module Plutonium
279
315
 
280
316
  def render_footer
281
317
  div(class: "lg:sticky bottom-[-2px] mt-1 p-4 pb-6 w-full z-30 bg-[var(--pu-body)]") {
282
- TableInfo(pagy_instance)
318
+ TableInfo(pagy_instance, item_name: resource_name(resource_class, pagy_instance.count))
283
319
  TablePagination(pagy_instance)
284
320
  }
285
321
  end
@@ -69,7 +69,7 @@ module Plutonium
69
69
 
70
70
  div(class: "pu-wizard-review-outstanding rounded-lg border border-warning-300 bg-warning-50 dark:border-warning-800 dark:bg-warning-950/30 p-4 mb-6", role: "alert") do
71
71
  p(class: "text-sm font-medium text-[var(--pu-text)] mb-2") do
72
- "Some steps still need attention before you can finish:"
72
+ t("plutonium.wizard.review.outstanding")
73
73
  end
74
74
  ul(class: "space-y-1") do
75
75
  incomplete.each do |step|
@@ -78,7 +78,7 @@ module Plutonium
78
78
  href: @step_url.call(step.key),
79
79
  class: "text-sm font-medium text-primary-600 dark:text-primary-400 hover:underline",
80
80
  data: {wizard_review_fix: step.key}
81
- ) { "Fix #{step.label}" }
81
+ ) { t("plutonium.wizard.review.fix_step", step: step.label) }
82
82
  end
83
83
  end
84
84
  end
@@ -109,7 +109,7 @@ module Plutonium
109
109
  a(
110
110
  href: @step_url.call(step.key),
111
111
  class: "shrink-0 text-xs font-medium text-primary-600 dark:text-primary-400 hover:underline"
112
- ) { "Edit" }
112
+ ) { t("plutonium.wizard.review.edit") }
113
113
  end
114
114
  div(class: "px-5 py-4") do
115
115
  # Decorate so attachment fields resolve to displayable attachments —
@@ -147,14 +147,14 @@ module Plutonium
147
147
  name.to_s.humanize
148
148
  end
149
149
  if rows.empty?
150
- p(class: "text-sm text-[var(--pu-text-subtle)]") { "None" }
150
+ p(class: "text-sm text-[var(--pu-text-subtle)]") { t("plutonium.wizard.review.none") }
151
151
  else
152
152
  ul(class: "space-y-1.5") do
153
153
  rows.each do |row|
154
154
  li(class: "flex flex-wrap items-center gap-x-2 gap-y-0.5 rounded-[var(--pu-radius-md)] bg-[var(--pu-surface-alt)] px-3 py-2 text-sm text-[var(--pu-text)]") do
155
155
  row.each do |key, value|
156
156
  span do
157
- span(class: "text-[var(--pu-text-muted)]") { "#{key.to_s.humanize}: " }
157
+ span(class: "text-[var(--pu-text-muted)]") { t("plutonium.wizard.review.row_label", label: key.to_s.humanize) }
158
158
  plain value.to_s
159
159
  end
160
160
  end
@@ -186,9 +186,9 @@ module Plutonium
186
186
  div(class: "mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-success-100 text-success-600 dark:bg-success-900/30 dark:text-success-400") do
187
187
  render Phlex::TablerIcons::Check.new(class: "h-7 w-7")
188
188
  end
189
- h3(class: "text-lg font-semibold tracking-tight text-[var(--pu-text)]") { "You're all set" }
189
+ h3(class: "text-lg font-semibold tracking-tight text-[var(--pu-text)]") { t("plutonium.wizard.review.ready_title") }
190
190
  p(class: "mt-1.5 max-w-prose text-sm text-[var(--pu-text-muted)]") do
191
- "Everything looks good. Click Finish to complete."
191
+ t("plutonium.wizard.review.ready_message")
192
192
  end
193
193
  end
194
194
  end
@@ -31,7 +31,7 @@ module Plutonium
31
31
  end
32
32
 
33
33
  def view_template
34
- nav(class: "pu-wizard-stepper mb-7", aria_label: "Progress") do
34
+ nav(class: "pu-wizard-stepper mb-7", aria_label: t("plutonium.wizard.stepper.progress")) do
35
35
  ol(class: "pu-wizard-steps") do
36
36
  @steps.each_with_index do |step, index|
37
37
  render_step(step, index)
@@ -38,6 +38,12 @@ module Plutonium
38
38
 
39
39
  def render_summary_field(name)
40
40
  input_options = @summary_inputs[name]&.dig(:options) || {}
41
+ # Summarise only what the step showed: an input hidden by its own
42
+ # `condition:` was never answered, so it gets no (empty) row. The
43
+ # condition runs against this display exactly as it ran against the
44
+ # step form — `object` is the same staged data either way.
45
+ condition = input_options[:condition]
46
+ return if condition && !instance_exec(&condition)
41
47
  field_options = input_options[:label] ? {label: input_options[:label]} : {}
42
48
 
43
49
  # A currency input stages a plain decimal; the data snapshot carries no
@@ -1,5 +1,5 @@
1
1
  module Plutonium
2
- VERSION = "0.64.0"
2
+ VERSION = "0.65.0"
3
3
  NEXT_MAJOR_VERSION = VERSION.split(".").tap { |v|
4
4
  v[1] = v[1].to_i + 1
5
5
  v[2] = 0
@@ -27,6 +27,8 @@ module Plutonium
27
27
  # end
28
28
  # end
29
29
  class Base
30
+ extend Plutonium::Translation::Lazy
31
+
30
32
  include ActiveModel::Model
31
33
  include Plutonium::Definition::Presentable
32
34
  include DSL
@@ -33,6 +33,8 @@ module Plutonium
33
33
  # other.
34
34
  WIZARD_RETURN_TO_KEY = "plutonium_wizard_return_to"
35
35
 
36
+ AuthorizationDeniedResult = Struct.new(:message)
37
+
36
38
  # The per-wizard key under {SESSION_TOKENS_KEY} for a guest run's token.
37
39
  def self.session_token_key(wizard_class)
38
40
  wizard_class.name.underscore.tr("/", "_")
@@ -516,7 +518,7 @@ module Plutonium
516
518
  def deny_wizard_resume_for_other_user!(runner)
517
519
  return unless runner.forbidden?
518
520
 
519
- raise ActiveRecord::RecordNotFound, "wizard run not found"
521
+ raise ActiveRecord::RecordNotFound, I18n.t("plutonium.wizard.run_not_found")
520
522
  end
521
523
 
522
524
  # Entry auth (§5.2 / §6.5). A wizard may define `authorize?` (default allow);
@@ -528,7 +530,11 @@ module Plutonium
528
530
  wizard = runner.wizard
529
531
  return if wizard.authorize?
530
532
 
531
- raise ActionPolicy::Unauthorized.new(wizard.class, :authorize?)
533
+ raise ::ActionPolicy::Unauthorized.new(
534
+ wizard,
535
+ :authorize?,
536
+ AuthorizationDeniedResult.new(I18n.t("plutonium.wizard.authorization_denied"))
537
+ )
532
538
  end
533
539
 
534
540
  # --- params ---
@@ -44,7 +44,8 @@ module Plutonium
44
44
  fields: capture,
45
45
  on_submit: capture.delete_hook(:on_submit),
46
46
  on_rollback: capture.delete_hook(:on_rollback),
47
- using_spec: capture.using_spec
47
+ using_spec: capture.using_spec,
48
+ wizard_class: self
48
49
  )
49
50
  end
50
51
 
@@ -60,7 +61,7 @@ module Plutonium
60
61
  # `header:` (default true) controls the step-header section (the label +
61
62
  # the "check everything over" prompt). `header: false` drops it entirely,
62
63
  # leaving just the review body in the card — for a chromeless finish.
63
- def review(label: "Review", description: nil, condition: nil, summary: true, header: true, &block)
64
+ def review(label: nil, description: nil, condition: nil, summary: true, header: true, &block)
64
65
  assert_not_after_review!(:review)
65
66
  steps << ReviewStep.new(label:, description:, condition:, summary:, header:, block:)
66
67
  end
@@ -344,6 +345,7 @@ module Plutonium
344
345
  subclass.instance_variable_set(:@anchor_resolver, @anchor_resolver)
345
346
  subclass.instance_variable_set(:@navigation, @navigation)
346
347
  subclass.instance_variable_set(:@stepper, @stepper)
348
+ subclass.instance_variable_set(:@width, @width)
347
349
  subclass.instance_variable_set(:@on_relaunch, @on_relaunch)
348
350
  subclass.instance_variable_set(:@cleanup_after, @cleanup_after)
349
351
  subclass.instance_variable_set(:@cleanup_after_set, @cleanup_after_set)
@@ -36,14 +36,7 @@ module Plutonium
36
36
  FORM_VALIDATOR_KINDS = %i[presence length numericality format inclusion].freeze
37
37
 
38
38
  # The resolved import surface for one `using:` declaration.
39
- Spec = Struct.new(:attribute_schema, :inputs, :form_layout, :validate_fn, :form_validators) do
40
- # Run the imported validation over a staged data slice, returning a hash of
41
- # {attribute => [messages]} for the imported fields + :base. Empty when
42
- # `validate: false`.
43
- def validate(data_slice)
44
- validate_fn ? validate_fn.call(data_slice) : {}
45
- end
46
- end
39
+ Spec = Struct.new(:attribute_schema, :inputs, :form_layout, :validate_fn, :form_validators)
47
40
 
48
41
  class << self
49
42
  # @param using [Class] an ActiveRecord model class
@@ -27,13 +27,20 @@ module Plutonium
27
27
 
28
28
  attr_reader :block, :summary, :header
29
29
 
30
- def initialize(key: :review, label: "Review", description: nil, condition: nil, summary: true, header: true, block: nil)
30
+ def initialize(key: :review, label: nil, description: nil, condition: nil, summary: true, header: true, block: nil)
31
31
  super(key:, label:, description:, condition:, fields: EmptyFields.new)
32
+ @explicit_label = label
32
33
  @summary = summary
33
34
  @header = header
34
35
  @block = block
35
36
  end
36
37
 
38
+ # The author's `label:` when given; otherwise the translated default,
39
+ # looked up per call so it follows the request's locale.
40
+ def label
41
+ Plutonium::Translation.resolve(@explicit_label) || I18n.t("plutonium.wizard.review.label")
42
+ end
43
+
37
44
  def review? = true
38
45
 
39
46
  # Whether the auto-summary of completed steps renders in the COMPLETE state
@@ -6,13 +6,14 @@ module Plutonium
6
6
  # field surface, per-step hooks, and the `using:` import marker (resolved in
7
7
  # Task 3). A value object — holds no runtime state.
8
8
  class Step
9
- attr_reader :key, :label, :description, :condition, :fields,
9
+ attr_reader :key, :condition, :fields,
10
10
  :on_submit, :on_rollback, :using_spec
11
11
 
12
12
  def initialize(key:, fields:, label: nil, description: nil, condition: nil,
13
- on_submit: nil, on_rollback: nil, using_spec: nil)
13
+ on_submit: nil, on_rollback: nil, using_spec: nil, wizard_class: nil)
14
14
  @key = key
15
- @label = label || key.to_s.humanize
15
+ @label = label
16
+ @wizard_class = wizard_class
16
17
  @description = description
17
18
  @condition = condition
18
19
  @fields = fields
@@ -21,6 +22,16 @@ module Plutonium
21
22
  @using_spec = using_spec
22
23
  end
23
24
 
25
+ # `label:` if given, else plutonium.wizard_steps.<wizard>.<key>, else the
26
+ # humanized key. Resolved on every read so it follows the request locale.
27
+ def label
28
+ Plutonium::Translation.resolve(@label) ||
29
+ Plutonium::Translation.label_for(:wizard_step, @wizard_class, key) ||
30
+ key.to_s.humanize
31
+ end
32
+
33
+ def description = Plutonium::Translation.resolve(@description)
34
+
24
35
  def review? = false
25
36
 
26
37
  # The step's form sections (§7.1): inline `form_layout` wins, else the layout
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@radioactive-labs/plutonium",
3
- "version": "0.64.0",
3
+ "version": "0.65.0",
4
4
  "description": "Build production-ready Rails apps in minutes, not days. Convention-driven, fully customizable, AI-ready.",
5
5
  "type": "module",
6
6
  "main": "src/js/core.js",
@@ -25,6 +25,8 @@
25
25
  "@uppy/dashboard": "^4.1.3",
26
26
  "@uppy/image-editor": "^3.2.1",
27
27
  "@uppy/xhr-upload": "^4.2.3",
28
+ "chart.js": "^4.5.1",
29
+ "chartkick": "^5.0.1",
28
30
  "dompurify": "^3.4.11",
29
31
  "lodash.debounce": "^4.0.8",
30
32
  "marked": "^15.0.3"
@@ -55,7 +57,7 @@
55
57
  },
56
58
  "scripts": {
57
59
  "dev": "concurrently \"yarn css:dev\" \"yarn js:dev\"",
58
- "build": "yarn js:prod && yarn css:prod",
60
+ "build": "rm -rf src/dist && yarn js:prod && yarn css:prod",
59
61
  "prepare": "yarn build",
60
62
  "css:dev": "chokidar \"src/css/**/*.css\" \"src/js/**/*.js\" \"app/views/**/*.{rb,erb,js}\" \"lib/plutonium/**/*.rb\" -c \"postcss src/css/plutonium.entry.css -o src/build/plutonium.css --dev\" --initial",
61
63
  "js:dev": "node esbuild.config.js --dev",
data/plutonium.gemspec CHANGED
@@ -32,7 +32,10 @@ Gem::Specification.new do |spec|
32
32
  spec.files = Dir.chdir(__dir__) do
33
33
  `git ls-files -z`.split("\x0").reject do |f|
34
34
  (File.expand_path(f) == __FILE__) ||
35
- f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
35
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile]) ||
36
+ # Maintainer tooling; host apps read tailwind.options.js and the postcss
37
+ # plugin from the gem, not the lockfile or yarn settings.
38
+ %w[.yarnrc.yml yarn.lock public/plutonium-assets/].any? { |prefix| f.start_with?(prefix) }
36
39
  end
37
40
  end
38
41
  spec.bindir = "exe"
@@ -0,0 +1,34 @@
1
+ // CommonJS on purpose: host apps load this by absolute path from their
2
+ // postcss.config.js. This directory's package.json declares "type": "module",
3
+ // so a .js file here is ESM, which the require()-based loader in
4
+ // postcss-load-config 4 (postcss-cli 10) passes to PostCSS as a module
5
+ // namespace instead of a plugin. A .cjs file loads under require() and
6
+ // import() alike.
7
+ const { execSync } = require('child_process');
8
+
9
+ const plugin = (opts = {}) => {
10
+ return {
11
+ postcssPlugin: 'postcss-gem-import',
12
+ Once(root) {
13
+ root.walkAtRules('import', (rule) => {
14
+ const importPath = rule.params.replace(/['"]/g, '');
15
+
16
+ if (importPath.startsWith('gem:')) {
17
+ const gemName = importPath.split('gem:')[1].split('/')[0];
18
+
19
+ try {
20
+ const gemPath = execSync(`bundle show ${gemName}`, { encoding: 'utf8' }).trim();
21
+ const newPath = importPath.replace(`gem:${gemName}`, gemPath);
22
+ rule.params = `"${newPath}"`;
23
+ } catch (error) {
24
+ throw rule.error(`Failed to resolve gem path for ${gemName}: ${error.message}`);
25
+ }
26
+ }
27
+ });
28
+ }
29
+ };
30
+ };
31
+
32
+ plugin.postcss = true;
33
+
34
+ module.exports = plugin;
@@ -1,28 +1,3 @@
1
- import { execSync } from 'child_process';
2
-
3
- const plugin = (opts = {}) => {
4
- return {
5
- postcssPlugin: 'postcss-gem-import',
6
- Once(root) {
7
- root.walkAtRules('import', (rule) => {
8
- const importPath = rule.params.replace(/['"]/g, '');
9
-
10
- if (importPath.startsWith('gem:')) {
11
- const gemName = importPath.split('gem:')[1].split('/')[0];
12
-
13
- try {
14
- const gemPath = execSync(`bundle show ${gemName}`, { encoding: 'utf8' }).trim();
15
- const newPath = importPath.replace(`gem:${gemName}`, gemPath);
16
- rule.params = `"${newPath}"`;
17
- } catch (error) {
18
- throw rule.error(`Failed to resolve gem path for ${gemName}: ${error.message}`);
19
- }
20
- }
21
- });
22
- }
23
- };
24
- };
25
-
26
- plugin.postcss = true;
27
-
28
- export default plugin;
1
+ // Apps generated before the plugin moved to CommonJS reference this path from
2
+ // their postcss.config.js. Keep it resolving; new apps point at the .cjs file.
3
+ export { default } from "./postcss-gem-import.cjs"
@@ -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
  =================== */
@@ -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
- }