plutonium 0.52.0 → 0.53.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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium-resource/SKILL.md +6 -4
  3. data/.claude/skills/plutonium-tenancy/SKILL.md +9 -4
  4. data/.claude/skills/plutonium-ui/SKILL.md +29 -5
  5. data/CHANGELOG.md +16 -0
  6. data/app/assets/plutonium.css +1 -1
  7. data/app/assets/plutonium.js +257 -11
  8. data/app/assets/plutonium.js.map +4 -4
  9. data/app/assets/plutonium.min.js +39 -39
  10. data/app/assets/plutonium.min.js.map +4 -4
  11. data/app/views/plutonium/_resource_header.html.erb +2 -1
  12. data/docs/.vitepress/config.ts +1 -0
  13. data/docs/guides/authentication.md +1 -1
  14. data/docs/guides/custom-actions.md +2 -1
  15. data/docs/guides/customizing-ui.md +6 -5
  16. data/docs/guides/multi-tenancy.md +6 -6
  17. data/docs/guides/theming.md +1 -1
  18. data/docs/public/images/components/avatar.png +0 -0
  19. data/docs/reference/auth/accounts.md +1 -1
  20. data/docs/reference/behavior/policies.md +1 -1
  21. data/docs/reference/configuration.md +61 -0
  22. data/docs/reference/resource/actions.md +2 -1
  23. data/docs/reference/resource/definition.md +4 -3
  24. data/docs/reference/tenancy/entity-scoping.md +12 -13
  25. data/docs/reference/ui/components.md +53 -0
  26. data/docs/reference/ui/forms.md +1 -1
  27. data/docs/reference/ui/pages.md +6 -5
  28. data/docs/superpowers/specs/2026-05-29-avatar-component-design.md +153 -0
  29. data/gemfiles/rails_7.gemfile.lock +1 -1
  30. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  31. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  32. data/lib/generators/pu/lite/solid_errors/solid_errors_generator.rb +7 -3
  33. data/lib/plutonium/action/base.rb +43 -63
  34. data/lib/plutonium/configuration.rb +7 -0
  35. data/lib/plutonium/definition/actions.rb +10 -11
  36. data/lib/plutonium/definition/base.rb +29 -0
  37. data/lib/plutonium/helpers/assets_helper.rb +0 -30
  38. data/lib/plutonium/helpers/content_helper.rb +0 -44
  39. data/lib/plutonium/helpers/display_helper.rb +0 -62
  40. data/lib/plutonium/helpers/turbo_helper.rb +0 -4
  41. data/lib/plutonium/helpers.rb +0 -2
  42. data/lib/plutonium/resource/definition.rb +0 -42
  43. data/lib/plutonium/ui/action_button.rb +4 -3
  44. data/lib/plutonium/ui/avatar.rb +182 -0
  45. data/lib/plutonium/ui/component/kit.rb +2 -0
  46. data/lib/plutonium/ui/form/base.rb +16 -2
  47. data/lib/plutonium/ui/form/components/secure_association.rb +3 -2
  48. data/lib/plutonium/ui/form/resource.rb +58 -0
  49. data/lib/plutonium/ui/form/theme.rb +7 -3
  50. data/lib/plutonium/ui/grid/card.rb +10 -26
  51. data/lib/plutonium/ui/modal/base.rb +36 -1
  52. data/lib/plutonium/ui/modal/centered.rb +24 -6
  53. data/lib/plutonium/ui/modal/slideover.rb +26 -11
  54. data/lib/plutonium/ui/nav_user.rb +3 -23
  55. data/lib/plutonium/ui/page/edit.rb +6 -3
  56. data/lib/plutonium/ui/page/interactive_action.rb +5 -3
  57. data/lib/plutonium/ui/page/new.rb +6 -3
  58. data/lib/plutonium/ui/table/components/bulk_actions_toolbar.rb +1 -1
  59. data/lib/plutonium/version.rb +1 -1
  60. data/package.json +1 -1
  61. data/src/css/components.css +38 -1
  62. data/src/css/slim_select.css +3 -2
  63. data/src/js/controllers/dirty_form_guard_controller.js +165 -0
  64. data/src/js/controllers/register_controllers.js +2 -0
  65. data/src/js/controllers/remote_modal_controller.js +53 -19
  66. data/src/js/turbo/index.js +1 -0
  67. data/src/js/turbo/turbo_confirm.js +128 -0
  68. metadata +10 -6
  69. data/lib/plutonium/helpers/attachment_helper.rb +0 -73
  70. data/lib/plutonium/helpers/table_helper.rb +0 -35
  71. /data/lib/generators/pu/rodauth/templates/app/views/rodauth_mailer/{password_changed.text.erb → change_password_notify.text.erb} +0 -0
@@ -5,41 +5,9 @@ require "active_support/string_inquirer"
5
5
  module Plutonium
6
6
  module Action
7
7
  # Base class for all actions in the Plutonium framework.
8
- #
9
- # @attr_reader [Symbol] name The name of the action.
10
- # @attr_reader [String] label The human-readable label for the action.
11
- # @attr_reader [String, nil] icon The icon associated with the action.
12
- # @attr_reader [RouteOptions] route_options The routing options for the action.
13
- # @attr_reader [String, nil] confirmation The confirmation message for the action.
14
- # @attr_reader [String, nil] turbo_frame The Turbo Frame ID for the action.
15
- # @attr_reader [Symbol, nil] color The color associated with the action.
16
- # @attr_reader [Symbol, nil] category The category of the action.
17
- # @attr_reader [Integer] position The position of the action within its category.
18
8
  class Base
19
- attr_reader :name, :label, :description, :icon, :route_options, :confirmation, :turbo, :turbo_frame, :color, :category, :position, :return_to, :modal
20
-
21
- # Initialize a new action.
22
- #
23
- # @param [Symbol] name The name of the action.
24
- # @param [Hash] options The options for the action.
25
- # @option options [String] :label The human-readable label for the action.
26
- # @option options [String] :description The human-readable description for the action.
27
- # @option options [String] :icon The icon associated with the action (e.g., 'fa-edit' for Font Awesome).
28
- # @option options [Symbol] :color The color associated with the action (e.g., :primary, :secondary, :success, :warning, :danger).
29
- # @option options [String] :confirmation The confirmation message to display before executing the action.
30
- # @option options [RouteOptions, Hash] :route_options The routing options for the action.
31
- # @option options [String] :turbo_frame The Turbo Frame ID for the action (used in Hotwire/Turbo Drive applications).
32
- # @option options [String, Symbol] :return_to Override the return_to URL for this action. If not provided, defaults to current URL.
33
- # @option options [Boolean] :bulk_action (false) If true, applies to a bulk selection of records (e.g., "Mark Selected as Read").
34
- # @option options [Boolean] :collection_record_action (false) If true, applies to records in a collection (e.g., "Edit Record" button in a table).
35
- # @option options [Boolean] :record_action (false) If true, applies to an individual record (e.g., "Delete" button on a Show page).
36
- # @option options [Boolean] :resource_action (false) If true, applies to the entire resource and can be used in any context (e.g., "Import from CSV").
37
- # @option options [Symbol] :category The category of the action. Determines visibility and grouping.
38
- # Valid values include:
39
- # @option options [Symbol] :primary Always shown and given prominence in the UI.
40
- # @option options [Symbol] :secondary Shown in secondary menus or less prominent areas.
41
- # @option options [Symbol] :danger Actions that require caution, often destructive operations.
42
- # @option options [Integer] :position (50) The position of the action in its group. Lower numbers appear first.
9
+ attr_reader :name, :label, :description, :icon, :route_options, :confirmation, :turbo, :color, :category, :position, :return_to
10
+
43
11
  def initialize(name, **options)
44
12
  @name = name.to_sym
45
13
  @label = options[:label] || @name.to_s.titleize
@@ -57,51 +25,53 @@ module Plutonium
57
25
  @resource_action = options[:resource_action] || false
58
26
  @category = ActiveSupport::StringInquirer.new((options[:category] || :secondary).to_s)
59
27
  @position = options[:position] || 50
60
- @modal = options[:modal] || :centered
61
- validate_modal!
28
+ @modal_mode = options[:modal]
29
+ @modal_size = options[:size]
30
+ validate_modal_mode!
31
+ validate_modal_size!
62
32
 
63
33
  freeze
64
34
  end
65
35
 
66
- # @return [Boolean] Whether this is a bulk action.
67
- def bulk_action?
68
- @bulk_action
36
+ # Resolves to the definition's `modal_mode` when unset on the action.
37
+ def modal_mode(definition = nil)
38
+ return @modal_mode if @modal_mode || definition.nil?
39
+ definition.modal_mode
69
40
  end
70
41
 
71
- # @return [Boolean] Whether this is a collection record action.
72
- def collection_record_action?
73
- @collection_record_action
42
+ # Resolves to the definition's `modal_size` when unset on the action.
43
+ def modal_size(definition = nil)
44
+ return @modal_size if @modal_size || definition.nil?
45
+ definition.modal_size
74
46
  end
75
47
 
76
- # @return [Boolean] Whether this is a record action.
77
- def record_action?
78
- @record_action
48
+ # Downgrades the remote-modal frame to nil when the definition has
49
+ # `modal false`, so the link navigates as a full page instead of
50
+ # targeting a frame that won't exist. Other frames pass through.
51
+ def turbo_frame(definition = nil)
52
+ return nil if definition && targets_remote_modal? && definition.modal_mode == false
53
+ @turbo_frame
79
54
  end
80
55
 
81
- # @return [Boolean] Whether this is a resource action.
82
- def resource_action?
83
- @resource_action
84
- end
56
+ def bulk_action? = @bulk_action
57
+ def collection_record_action? = @collection_record_action
58
+ def record_action? = @record_action
59
+ def resource_action? = @resource_action
85
60
 
86
61
  def permitted_by?(policy)
87
62
  policy.allowed_to?(:"#{name}?")
88
63
  end
89
64
 
90
65
  # Returns a new Action with the given options merged over this one.
91
- # Used by the resource definition to derive variants (e.g. dropping
92
- # `turbo_frame` when `modal false` is configured) without mutating
93
- # the frozen original.
94
66
  def with(**overrides)
95
67
  self.class.new(name, **to_options.merge(overrides))
96
68
  end
97
69
 
98
70
  protected
99
71
 
100
- # Canonical representation for reconstruction via `with`. Every
72
+ # Canonical option hash for reconstruction via `with`. Every
101
73
  # attribute set in `initialize` MUST appear here; otherwise
102
74
  # `with(**overrides)` would silently drop it on round-trip.
103
- # `category` is exposed as a Symbol since `initialize` re-wraps
104
- # it in StringInquirer.
105
75
  def to_options
106
76
  {
107
77
  label: @label,
@@ -119,21 +89,31 @@ module Plutonium
119
89
  resource_action: @resource_action,
120
90
  category: @category.to_sym,
121
91
  position: @position,
122
- modal: @modal
92
+ modal: @modal_mode,
93
+ size: @modal_size
123
94
  }
124
95
  end
125
96
 
126
97
  private
127
98
 
128
- def validate_modal!
129
- return if [:centered, :slideover].include?(@modal)
130
- raise ArgumentError, "modal must be :centered or :slideover, got #{@modal.inspect}"
99
+ def targets_remote_modal?
100
+ @turbo_frame == Plutonium::REMOTE_MODAL_FRAME
101
+ end
102
+
103
+ def validate_modal_mode!
104
+ return if @modal_mode.nil?
105
+ return if [:centered, :slideover].include?(@modal_mode)
106
+ raise ArgumentError, "modal must be :centered or :slideover, got #{@modal_mode.inspect}"
107
+ end
108
+
109
+ def validate_modal_size!
110
+ return if @modal_size.nil?
111
+ return if Plutonium::UI::Modal::Base::VALID_SIZES.include?(@modal_size)
112
+ raise ArgumentError,
113
+ "size must be one of #{Plutonium::UI::Modal::Base::VALID_SIZES.inspect}, " \
114
+ "got #{@modal_size.inspect}"
131
115
  end
132
116
 
133
- # Build RouteOptions from the provided options
134
- #
135
- # @param [RouteOptions, Hash, nil] options The routing options
136
- # @return [RouteOptions] The built RouteOptions object
137
117
  def build_route_options(options)
138
118
  case options
139
119
  when RouteOptions
@@ -30,6 +30,12 @@ module Plutonium
30
30
  # @return [Symbol] :classic (legacy Header/Sidebar) or :modern (Topbar/IconRail)
31
31
  attr_accessor :shell
32
32
 
33
+ # @return [String] host URL of the Navii avatar service (no path), used by
34
+ # {Plutonium::UI::Avatar} as the default profile-image fallback. The
35
+ # component appends the `/avatar/:seed` route. Repoint this to self-host
36
+ # or proxy the service.
37
+ attr_accessor :navii_host_url
38
+
33
39
  # Map of version numbers to their default configurations
34
40
  VERSION_DEFAULTS = {
35
41
  1.0 => proc do |config|
@@ -52,6 +58,7 @@ module Plutonium
52
58
  @cache_discovery = !Rails.env.development?
53
59
  @enable_hotreload = Rails.env.development?
54
60
  @shell = :modern
61
+ @navii_host_url = "https://api.navii.dev"
55
62
  end
56
63
 
57
64
  # Load default configuration for a specific version
@@ -6,19 +6,19 @@ module Plutonium
6
6
  included do
7
7
  defineable_prop :action
8
8
 
9
- def self.action(name, interaction: nil, **)
9
+ def self.action(name, interaction: nil, **opts)
10
10
  defined_actions[name] = if interaction
11
- Plutonium::Action::Interactive::Factory.create(name, interaction:, **)
11
+ Plutonium::Action::Interactive::Factory.create(name, interaction:, **opts)
12
12
  else
13
- Plutonium::Action::Simple.new(name, **)
13
+ Plutonium::Action::Simple.new(name, **opts)
14
14
  end
15
15
  end
16
16
 
17
- def action(name, interaction: nil, **)
17
+ def action(name, interaction: nil, **opts)
18
18
  instance_defined_actions[name] = if interaction
19
- Plutonium::Action::Interactive::Factory.create(name, interaction:, **)
19
+ Plutonium::Action::Interactive::Factory.create(name, interaction:, **opts)
20
20
  else
21
- Plutonium::Action::Simple.new(name, **)
21
+ Plutonium::Action::Simple.new(name, **opts)
22
22
  end
23
23
  end
24
24
 
@@ -32,12 +32,10 @@ module Plutonium
32
32
 
33
33
  # standard CRUD actions
34
34
 
35
- # turbo_frame for :new and :edit is set by
36
- # Resource::Definition.configure_crud_modal_targets! based on the
37
- # `modal` config. Don't hard-code it here.
38
35
  action(:new, route_options: {action: :new},
39
36
  resource_action: true, category: :primary,
40
- icon: Phlex::TablerIcons::Plus, position: 10)
37
+ icon: Phlex::TablerIcons::Plus, position: 10,
38
+ turbo_frame: Plutonium::REMOTE_MODAL_FRAME)
41
39
 
42
40
  action(:show, route_options: {action: :show},
43
41
  collection_record_action: true, category: :primary,
@@ -45,7 +43,8 @@ module Plutonium
45
43
 
46
44
  action(:edit, route_options: {action: :edit},
47
45
  record_action: true, collection_record_action: true, category: :primary,
48
- icon: Phlex::TablerIcons::Edit, position: 20)
46
+ icon: Phlex::TablerIcons::Edit, position: 20,
47
+ turbo_frame: Plutonium::REMOTE_MODAL_FRAME)
49
48
 
50
49
  action(:destroy, route_options: {method: :delete},
51
50
  record_action: true, collection_record_action: true, category: :danger,
@@ -86,6 +86,35 @@ module Plutonium
86
86
  # false = always hide
87
87
  inheritable_config_attr :submit_and_continue
88
88
 
89
+ # modals — drive how :new / :edit and interactive actions render.
90
+ # Actions read these lazily at render time, so override order and
91
+ # subclass inheritance both work naturally.
92
+ VALID_MODAL_MODES = [:centered, :slideover, false].freeze
93
+
94
+ inheritable_config_attr :modal_mode, :modal_size
95
+ modal_mode :slideover
96
+ modal_size :md
97
+
98
+ # Sets `modal_mode` and `modal_size` together with validation.
99
+ #
100
+ # - :slideover (default) — slide-in panel from the right
101
+ # - :centered — centered dialog
102
+ # - false — no modal; new/edit are full standalone pages
103
+ #
104
+ # `size:` see Plutonium::UI::Modal::Base::VALID_SIZES. `:auto`
105
+ # hugs the form's natural width.
106
+ def self.modal(mode, size: :md)
107
+ unless VALID_MODAL_MODES.include?(mode)
108
+ raise ArgumentError, "modal must be one of #{VALID_MODAL_MODES.inspect}, got #{mode.inspect}"
109
+ end
110
+ unless Plutonium::UI::Modal::Base::VALID_SIZES.include?(size)
111
+ raise ArgumentError,
112
+ "modal size must be one of #{Plutonium::UI::Modal::Base::VALID_SIZES.inspect}, got #{size.inspect}"
113
+ end
114
+ modal_mode mode
115
+ modal_size size
116
+ end
117
+
89
118
  def initialize
90
119
  super
91
120
  end
@@ -4,29 +4,6 @@ module Plutonium
4
4
  module Helpers
5
5
  # Helper module for managing asset-related functionality
6
6
  module AssetsHelper
7
- # Generate a stylesheet tag for the resource
8
- #
9
- # @return [ActiveSupport::SafeBuffer] HTML stylesheet link tag
10
- def resource_stylesheet_tag
11
- url = resource_asset_url_for(:css, resource_stylesheet_asset)
12
- stylesheet_link_tag(url, "data-turbo-track": "reload")
13
- end
14
-
15
- # Generate a script tag for the resource
16
- #
17
- # @return [ActiveSupport::SafeBuffer] HTML script tag
18
- def resource_script_tag
19
- url = resource_asset_url_for(:js, resource_script_asset)
20
- javascript_include_tag(url, "data-turbo-track": "reload", type: "module")
21
- end
22
-
23
- # Generate a favicon link tag
24
- #
25
- # @return [ActiveSupport::SafeBuffer] HTML favicon link tag
26
- def resource_favicon_tag
27
- favicon_link_tag(resource_favicon_asset)
28
- end
29
-
30
7
  # Generate an image tag for the logo
31
8
  #
32
9
  # @param classname [String] CSS class name for the image tag
@@ -56,13 +33,6 @@ module Plutonium
56
33
  Plutonium.configuration.assets.script
57
34
  end
58
35
 
59
- # Get the favicon asset path
60
- #
61
- # @return [String] path to the favicon asset
62
- def resource_favicon_asset
63
- Plutonium.configuration.assets.favicon
64
- end
65
-
66
36
  private
67
37
 
68
38
  # Generate the appropriate asset URL based on the environment
@@ -17,50 +17,6 @@ module Plutonium
17
17
  }
18
18
  )
19
19
  end
20
-
21
- def read_more(content, clamp = 4)
22
- return if content.blank?
23
-
24
- # Stimulus Read More (https://www.stimulus-components.com/docs/stimulus-read-more/)
25
- style = "overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; " \
26
- "-webkit-line-clamp: var(--read-more-line-clamp, #{clamp});"
27
-
28
- tag.div(
29
- data: {
30
- controller: "read-more",
31
- read_more_more_text_value: "Read more",
32
- read_more_less_text_value: "Read less"
33
- }
34
- ) do
35
- concat tag.div(content,
36
- style:,
37
- data: {read_more_target: "content"})
38
-
39
- next unless content.lines.size > clamp
40
-
41
- concat tag.button("Read more",
42
- class: "btn btn-sm btn-link text-decoration-none ps-0",
43
- data: {action: "read-more#toggle"})
44
- end
45
- end
46
-
47
- def quill(content)
48
- return if content.blank?
49
-
50
- tag.div(
51
- content,
52
- class: "ql-viewer",
53
- data: {
54
- controller: "quill-viewer"
55
- }
56
- )
57
- end
58
-
59
- def clamp_content(content)
60
- return if content.blank?
61
-
62
- tag.div content, class: "clamped-content"
63
- end
64
20
  end
65
21
  end
66
22
  end
@@ -1,11 +1,6 @@
1
1
  module Plutonium
2
2
  module Helpers
3
3
  module DisplayHelper
4
- # def tooltip(text)
5
- # text = sanitize text
6
- # "title=\"#{text}\" data-controller=\"tooltip\" data-bs-title=\"#{text}\"".html_safe
7
- # end
8
-
9
4
  def resource_name(resource_class, count = 1)
10
5
  resource_class.model_name.human.pluralize(count)
11
6
  end
@@ -32,59 +27,10 @@ module Plutonium
32
27
  end
33
28
  end
34
29
 
35
- def display_field(value:, helper: nil, **options)
36
- return "-" unless value.present?
37
-
38
- stack_multiple = options.key?(:stack_multiple) ? options.delete(:stack_multiple) : helper != :display_name_of
39
-
40
- # clean options list
41
- options.select! { |k, _v| !k.starts_with? "pu_" }
42
-
43
- if value.respond_to?(:each) && stack_multiple
44
- tag.ul class: "list-unstyled m-0" do
45
- value.each do |val|
46
- rendered = display_field_value(value: val, helper:, **options)
47
- concat tag.li(rendered)
48
- end
49
- end
50
- else
51
- rendered = display_field_value(value:, helper:, **options)
52
- tag.span rendered
53
- end
54
- end
55
-
56
30
  def display_datetime_value(value)
57
31
  timeago value
58
32
  end
59
33
 
60
- def display_field_value(value:, helper: nil, title: nil, **)
61
- title = (title != false) ? title || display_name_of(value) : nil
62
- rendered = helper.present? ? send(helper, value, **) : value
63
- tag.span rendered, title:
64
- end
65
-
66
- def display_association_value(association)
67
- display_name = display_name_of(association)
68
- if registered_resources.include?(association.class)
69
- link_to display_name, resource_url_for(association, parent: nil),
70
- class: "font-medium text-primary-600 dark:text-primary-500"
71
- else
72
- display_name
73
- end
74
- end
75
-
76
- def display_numeric_value(value)
77
- number_with_delimiter value
78
- end
79
-
80
- def display_boolean_value(value)
81
- tag.input type: :checkbox, class: "form-check-input", checked: value, disabled: true
82
- end
83
-
84
- def display_url_value(value)
85
- link_to nil, value, class: "font-medium text-primary-600 dark:text-primary-500", target: :blank
86
- end
87
-
88
34
  def display_name_of(obj, separator: ", ")
89
35
  return unless obj.present?
90
36
 
@@ -103,14 +49,6 @@ module Plutonium
103
49
  # Oh well. Just convert it to a string.
104
50
  obj.to_s
105
51
  end
106
-
107
- def display_clamped_quill(value)
108
- clamp_content quill(value)
109
- end
110
-
111
- def display_attachment_value(value, **, &)
112
- attachment_preview(value, **, &)
113
- end
114
52
  end
115
53
  end
116
54
  end
@@ -16,10 +16,6 @@ module Plutonium
16
16
  # modal frame specifically.
17
17
  def in_secondary_modal? = current_turbo_frame == Plutonium::REMOTE_MODAL_SECONDARY_FRAME
18
18
 
19
- def remote_modal_frame_tag(&)
20
- turbo_frame_tag(Plutonium::REMOTE_MODAL_FRAME, &)
21
- end
22
-
23
19
  # Returns a turbo-frame-scoped element id. Two identically-named forms
24
20
  # can be on the page simultaneously (e.g. a primary modal opens a
25
21
  # secondary modal, each rendering an `id="resource-form"`). When the
@@ -3,10 +3,8 @@ module Plutonium
3
3
  def self.included(base)
4
4
  base.class_eval do
5
5
  include Plutonium::Helpers::ApplicationHelper
6
- include Plutonium::Helpers::AttachmentHelper
7
6
  include Plutonium::Helpers::ContentHelper
8
7
  include Plutonium::Helpers::DisplayHelper
9
- include Plutonium::Helpers::TableHelper
10
8
  include Plutonium::Helpers::TurboHelper
11
9
  include Plutonium::Helpers::TurboStreamActionsHelper
12
10
  include Plutonium::Helpers::AssetsHelper
@@ -1,48 +1,6 @@
1
1
  module Plutonium
2
2
  module Resource
3
3
  class Definition < Plutonium::Definition::Base
4
- class_attribute :modal_mode, default: :slideover, instance_accessor: false
5
-
6
- VALID_MODAL_MODES = [:centered, :slideover, false].freeze
7
-
8
- # Sets how :new / :edit actions render.
9
- # - :slideover (default) — slide-in panel from the right
10
- # - :centered — centered dialog
11
- # - false — no modal; new/edit are full standalone pages
12
- def self.modal(mode)
13
- unless VALID_MODAL_MODES.include?(mode)
14
- raise ArgumentError, "modal must be one of #{VALID_MODAL_MODES.inspect}, got #{mode.inspect}"
15
- end
16
- self.modal_mode = mode
17
- configure_crud_modal_targets!
18
- end
19
-
20
- # Re-derives the default :new / :edit actions so their turbo_frame
21
- # matches the current `modal_mode`. Called when `.modal` is set
22
- # and once at Resource::Definition load (so the default
23
- # :slideover state propagates to the action records). Subclasses
24
- # inherit those records via DefineableProps#inherited (deep_dup);
25
- # calling `.modal` on a subclass re-runs this method locally.
26
- def self.configure_crud_modal_targets!
27
- target = (modal_mode == false) ? nil : Plutonium::REMOTE_MODAL_FRAME
28
- [:new, :edit].each do |name|
29
- action = defined_actions[name]
30
- next unless action
31
- next if action.turbo_frame == target
32
- defined_actions[name] = action.with(turbo_frame: target)
33
- end
34
- end
35
-
36
- def modal
37
- self.class.modal_mode
38
- end
39
-
40
- # Apply the default modal target ("remote_modal") to :new / :edit
41
- # so resources that never call `.modal` still get the slideover
42
- # behavior. Subclasses inherit the configured actions via
43
- # DefineableProps' deep_dup; calling `.modal` on a subclass
44
- # re-runs the configuration locally.
45
- configure_crud_modal_targets!
46
4
  end
47
5
  end
48
6
  end
@@ -50,7 +50,7 @@ module Plutonium
50
50
  link_to(
51
51
  url_with_return_to,
52
52
  class: button_classes,
53
- data: {turbo_frame: @action.turbo_frame}.merge(@extra_data)
53
+ data: {turbo_frame: @action.turbo_frame(current_definition)}.merge(@extra_data)
54
54
  ) do
55
55
  render_button_content
56
56
  end
@@ -67,7 +67,7 @@ module Plutonium
67
67
  data: {
68
68
  turbo: @action.turbo,
69
69
  turbo_confirm: @action.confirmation.presence,
70
- turbo_frame: @action.turbo_frame
70
+ turbo_frame: @action.turbo_frame(current_definition)
71
71
  }
72
72
  }
73
73
  ) do
@@ -84,7 +84,8 @@ module Plutonium
84
84
  }
85
85
 
86
86
  # Add turbo frame if specified
87
- link_attrs[:data] = {turbo_frame: @action.turbo_frame} if @action.turbo_frame
87
+ frame = @action.turbo_frame(current_definition)
88
+ link_attrs[:data] = {turbo_frame: frame} if frame
88
89
 
89
90
  # Add confirmation and method for non-GET requests
90
91
  if @action.confirmation || @action.route_options.method != :get