plutonium 0.26.4 → 0.26.7

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.
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Configure ActionPolicy for Plutonium
4
+ Rails.application.config.to_prepare do
5
+ # Install STI policy lookup support
6
+ # This allows STI models to use their base class's policy when a specific policy doesn't exist
7
+ require "plutonium/action_policy/sti_policy_lookup"
8
+ Plutonium::ActionPolicy::StiPolicyLookup.install!
9
+ end
@@ -190,6 +190,9 @@ class BlogDefinition < Plutonium::Resource::Definition
190
190
  # Configure sorting
191
191
  sort :title
192
192
  sort :published_at
193
+
194
+ # Configure default sorting (newest first)
195
+ default_sort :published_at, :desc
193
196
  end
194
197
  ```
195
198
 
@@ -43,6 +43,9 @@ class PostDefinition < Plutonium::Resource::Definition
43
43
  sort :title
44
44
  sort :created_at
45
45
  sort :view_count
46
+
47
+ # Define default sort (when no sort params are provided)
48
+ default_sort :created_at, :desc # or default_sort { |scope| scope.order(featured: :desc, created_at: :desc) }
46
49
  end
47
50
  ```
48
51
 
@@ -273,6 +276,13 @@ end
273
276
  - Consider performance impact of JOINs in search queries
274
277
  - Use `.distinct` when searching across associations
275
278
 
279
+ ### Default Sorting
280
+ - Define a default sort to show newest items first by default: `default_sort :id, :desc`
281
+ - Use field and direction: `default_sort :created_at, :desc`
282
+ - Or use a block for complex sorting: `default_sort { |scope| scope.order(featured: :desc, created_at: :desc) }`
283
+ - The default sort is only applied when no sort parameters are provided by the user
284
+ - Child definitions inherit the default sort from parent definitions
285
+
276
286
  ### URL Structure
277
287
  - The `q` parameter namespace keeps query params organized
278
288
  - All filter inputs are nested under their filter name
@@ -31,6 +31,9 @@ class PostDefinition < Plutonium::Resource::Definition
31
31
  # Define sorting
32
32
  sort :title
33
33
  sort :published_at
34
+
35
+ # Define default sort (newest first)
36
+ default_sort :created_at, :desc
34
37
 
35
38
  # Enable search
36
39
  search do |scope, query|
@@ -200,6 +203,11 @@ class PostDefinition < Plutonium::Resource::Definition
200
203
  sort :author_name, using: "users.name" do |scope, direction:|
201
204
  scope.joins(:author).order("users.name #{direction}")
202
205
  end
206
+
207
+ # Default sort when no user sorting is applied
208
+ default_sort :created_at, :desc # Simple form
209
+ # or with a block for complex sorting:
210
+ # default_sort { |scope| scope.order(featured: :desc, created_at: :desc) }
203
211
  end
204
212
  ```
205
213
 
@@ -16,7 +16,7 @@ module Plutonium
16
16
  # @attr_reader [Symbol, nil] category The category of the action.
17
17
  # @attr_reader [Integer] position The position of the action within its category.
18
18
  class Base
19
- attr_reader :name, :label, :description, :icon, :route_options, :confirmation, :turbo, :turbo_frame, :color, :category, :position
19
+ attr_reader :name, :label, :description, :icon, :route_options, :confirmation, :turbo, :turbo_frame, :color, :category, :position, :return_to
20
20
 
21
21
  # Initialize a new action.
22
22
  #
@@ -29,6 +29,7 @@ module Plutonium
29
29
  # @option options [String] :confirmation The confirmation message to display before executing the action.
30
30
  # @option options [RouteOptions, Hash] :route_options The routing options for the action.
31
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.
32
33
  # @option options [Boolean] :bulk_action (false) If true, applies to a bulk selection of records (e.g., "Mark Selected as Read").
33
34
  # @option options [Boolean] :collection_record_action (false) If true, applies to records in a collection (e.g., "Edit Record" button in a table).
34
35
  # @option options [Boolean] :record_action (false) If true, applies to an individual record (e.g., "Delete" button on a Show page).
@@ -49,6 +50,7 @@ module Plutonium
49
50
  @route_options = build_route_options(options[:route_options])
50
51
  @turbo = options[:turbo]
51
52
  @turbo_frame = options[:turbo_frame]
53
+ @return_to = options[:return_to]
52
54
  @bulk_action = options[:bulk_action] || false
53
55
  @collection_record_action = options[:collection_record_action] || false
54
56
  @record_action = options[:record_action] || false
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plutonium
4
+ module ActionPolicy
5
+ # Custom ActionPolicy lookup resolver for STI (Single Table Inheritance) models
6
+ # This resolver attempts to find a policy for the base class when a policy
7
+ # for the STI subclass is not found
8
+ module StiPolicyLookup
9
+ # STI base class policy resolver
10
+ # Checks if the record is an STI model and looks up the policy for its base class
11
+ STI_BASE_CLASS_LOOKUP = ->(record, namespace: nil, strict_namespace: false, **) {
12
+ # Skip if record is a symbol or doesn't have a class
13
+ next unless record.respond_to?(:class)
14
+
15
+ record_class = record.is_a?(Module) ? record : record.class
16
+
17
+ # Check if this is an STI model (has base_class and is different from current class)
18
+ next unless record_class.respond_to?(:base_class)
19
+ next if record_class == record_class.base_class
20
+
21
+ # Try to find policy for the base class
22
+ policy_name = "#{record_class.base_class}Policy"
23
+ ::ActionPolicy::LookupChain.send(:lookup_within_namespace, policy_name, namespace, strict: strict_namespace)
24
+ }
25
+
26
+ class << self
27
+ def install!
28
+ # Insert STI resolver before the standard INFER_FROM_CLASS resolver
29
+ # This ensures we try the base class before giving up
30
+ infer_index = ::ActionPolicy::LookupChain.chain.index(::ActionPolicy::LookupChain::INFER_FROM_CLASS)
31
+
32
+ if infer_index
33
+ # Insert after INFER_FROM_CLASS so it runs as a fallback
34
+ ::ActionPolicy::LookupChain.chain.insert(infer_index + 1, STI_BASE_CLASS_LOOKUP)
35
+ else
36
+ # If for some reason INFER_FROM_CLASS isn't found, append to end
37
+ ::ActionPolicy::LookupChain.chain << STI_BASE_CLASS_LOOKUP
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -73,13 +73,22 @@ module Plutonium
73
73
  if element.is_a?(Class)
74
74
  controller_chain << element.to_s.pluralize
75
75
  else
76
- controller_chain << element.class.to_s.pluralize
76
+ # For STI models, use the base class for routing if the specific class isn't registered
77
+ model_class = element.class
78
+ if model_class.respond_to?(:base_class) && model_class != model_class.base_class
79
+ # Check if the STI model is registered, if not use base class
80
+ route_configs = current_engine.routes.resource_route_config_for(model_class.to_s.pluralize.underscore)
81
+ model_class = model_class.base_class if route_configs.nil? || route_configs.empty?
82
+ end
83
+
84
+ controller_chain << model_class.to_s.pluralize
77
85
  if index == args.length - 1
78
- resource_route_config = current_engine.routes.resource_route_config_for(element.model_name.plural)[0]
79
- url_args[:id] = element.to_param unless resource_route_config[:route_type] == :resource
86
+ resource_route_configs = current_engine.routes.resource_route_config_for(model_class.to_s.pluralize.underscore)
87
+ resource_route_config = resource_route_configs&.first
88
+ url_args[:id] = element.to_param unless resource_route_config && resource_route_config[:route_type] == :resource
80
89
  url_args[:action] ||= :show
81
90
  else
82
- url_args[element.model_name.singular.to_sym] = element.to_param
91
+ url_args[model_class.to_s.underscore.singularize.to_sym] = element.to_param
83
92
  end
84
93
  end
85
94
  end
@@ -5,7 +5,7 @@ module Plutonium
5
5
  module Controllers
6
6
  module Authorizable
7
7
  extend ActiveSupport::Concern
8
- include ActionPolicy::Controller
8
+ include ::ActionPolicy::Controller
9
9
 
10
10
  included do
11
11
  authorize :user, through: :current_user
@@ -23,7 +23,7 @@ module Plutonium
23
23
  raise ArgumentError("Expected resource to be a class inheriting ActiveRecord::Base")
24
24
  end
25
25
 
26
- options[:with] ||= ActionPolicy.lookup(resource, namespace: authorization_namespace)
26
+ options[:with] ||= ::ActionPolicy.lookup(resource, namespace: authorization_namespace)
27
27
  relation ||= resource.all
28
28
 
29
29
  authorized_scope(relation, **options)
@@ -47,7 +47,7 @@ module Plutonium
47
47
  action(:destroy, route_options: {method: :delete},
48
48
  record_action: true, collection_record_action: true, category: :danger,
49
49
  icon: Phlex::TablerIcons::Trash, position: 100,
50
- confirmation: "Are you sure?", turbo_frame: "_top")
50
+ confirmation: "Are you sure?", turbo_frame: "_top", return_to: "")
51
51
 
52
52
  # Example of dynamic route options using custom url_resolver:
53
53
  #
@@ -6,9 +6,27 @@ module Plutonium
6
6
  included do
7
7
  defineable_props :sort
8
8
 
9
+ class_attribute :_default_sort, instance_writer: false, instance_predicate: false
10
+
9
11
  def self.sorts(*names)
10
12
  names.each { |name| sort name }
11
13
  end
14
+
15
+ def self.default_sort(field = nil, direction = :asc, &block)
16
+ self._default_sort = if block_given?
17
+ block
18
+ elsif field
19
+ [field, direction]
20
+ end
21
+ _default_sort
22
+ end
23
+
24
+ # Set a sensible default: newest items first
25
+ default_sort :id, :desc
26
+ end
27
+
28
+ def default_sort
29
+ self.class._default_sort
12
30
  end
13
31
  end
14
32
  end
@@ -54,7 +54,7 @@ module Plutonium
54
54
  config.after_initialize do
55
55
  Plutonium::Reloader.start! if Plutonium.configuration.enable_hotreload
56
56
  Plutonium::Loader.eager_load if Rails.env.production?
57
- ActionPolicy::PerThreadCache.enabled = !Rails.env.local?
57
+ ::ActionPolicy::PerThreadCache.enabled = !Rails.env.local?
58
58
  end
59
59
 
60
60
  private
@@ -20,10 +20,10 @@ module Plutonium
20
20
  extend ActiveSupport::Concern
21
21
 
22
22
  # Custom exception for missing authorize_current call
23
- class ActionMissingAuthorizeCurrent < ActionPolicy::UnauthorizedAction; end
23
+ class ActionMissingAuthorizeCurrent < ::ActionPolicy::UnauthorizedAction; end
24
24
 
25
25
  # Custom exception for missing current_authorized_scope call
26
- class ActionMissingCurrentAuthorizedScope < ActionPolicy::UnauthorizedAction; end
26
+ class ActionMissingCurrentAuthorizedScope < ::ActionPolicy::UnauthorizedAction; end
27
27
 
28
28
  included do
29
29
  after_action :verify_authorize_current
@@ -95,7 +95,7 @@ module Plutonium
95
95
 
96
96
  # Returns the policy for the current resource
97
97
  #
98
- # @return [ActionPolicy::Base] the policy for the current resource
98
+ # @return [::ActionPolicy::Base] the policy for the current resource
99
99
  def current_policy
100
100
  @current_policy ||= policy_for(record: current_policy_subject, context: current_policy_context)
101
101
  end
@@ -119,7 +119,7 @@ module Plutonium
119
119
  #
120
120
  # @param record [Object] the record to authorize
121
121
  # @param options [Hash] additional options for authorization
122
- # @raise [ActionPolicy::Unauthorized] if the action is not authorized
122
+ # @raise [::ActionPolicy::Unauthorized] if the action is not authorized
123
123
  def authorize_current!(record, **options)
124
124
  options[:context] = (options[:context] || {}).deep_merge(current_policy_context)
125
125
  authorize!(record, **options)
@@ -25,6 +25,10 @@ module Plutonium
25
25
  query_object.define_sorter key, value[:block], **value[:options]
26
26
  end
27
27
 
28
+ if current_definition.respond_to?(:default_sort) && current_definition.default_sort
29
+ query_object.default_sort_config = current_definition.default_sort
30
+ end
31
+
28
32
  current_definition.defined_filters.each do |key, value|
29
33
  with = value[:options][:with]
30
34
  if with.is_a?(Class) && with < Plutonium::Query::Filter
@@ -5,7 +5,7 @@ module Plutonium
5
5
  # Policy class to define permissions and attributes for a resource.
6
6
  # This class provides methods to check permissions for various actions
7
7
  # and to retrieve permitted attributes for these actions.
8
- class Policy < ActionPolicy::Base
8
+ class Policy < ::ActionPolicy::Base
9
9
  authorize :user, allow_nil: false
10
10
  authorize :entity_scope, allow_nil: true
11
11
 
@@ -2,6 +2,7 @@ module Plutonium
2
2
  module Resource
3
3
  class QueryObject
4
4
  attr_reader :search_filter, :search_query
5
+ attr_accessor :default_sort_config
5
6
 
6
7
  # Initializes a QueryObject with the given resource_class and parameters.
7
8
  #
@@ -221,12 +222,20 @@ module Plutonium
221
222
  # @return [Object] The modified scope.
222
223
  def apply_sorts(scope, params)
223
224
  selected_sort_directions = extract_sort_directions(params)
224
- selected_sort_fields.each do |name|
225
- next unless (sorter = sort_definitions[name])
226
225
 
227
- direction = selected_sort_directions[name] || "ASC"
228
- scope = sorter.apply(scope, direction:)
226
+ if selected_sort_fields.any?
227
+ # Apply user-selected sorts
228
+ selected_sort_fields.each do |name|
229
+ next unless (sorter = sort_definitions[name])
230
+
231
+ direction = selected_sort_directions[name] || "ASC"
232
+ scope = sorter.apply(scope, direction:)
233
+ end
234
+ elsif default_sort_config
235
+ # Apply default sort when no sorts are selected
236
+ scope = apply_default_sort(scope)
229
237
  end
238
+
230
239
  scope
231
240
  end
232
241
 
@@ -250,6 +259,24 @@ module Plutonium
250
259
  end
251
260
  end.compact
252
261
  end
262
+
263
+ # Applies the default sort to the given scope
264
+ #
265
+ # @param scope [Object] The initial scope
266
+ # @return [Object] The sorted scope
267
+ def apply_default_sort(scope)
268
+ case default_sort_config
269
+ when Proc
270
+ # Block form: default_sort { |scope| scope.order(...) }
271
+ default_sort_config.call(scope)
272
+ when Array
273
+ # Field/direction form: default_sort :created_at, :desc
274
+ field, direction = default_sort_config
275
+ scope.order(field => direction)
276
+ else
277
+ scope
278
+ end
279
+ end
253
280
  end
254
281
  end
255
282
  end
@@ -25,7 +25,7 @@ module Plutonium
25
25
  def render_link
26
26
  uri = URI.parse(@url)
27
27
  params = Rack::Utils.parse_nested_query(uri.query)
28
- params["return_to"] = request.original_url
28
+ params["return_to"] = @action.return_to.nil? ? request.original_url : @action.return_to
29
29
  uri.query = params.to_query
30
30
  uri.to_s
31
31
 
@@ -42,7 +42,7 @@ module Plutonium
42
42
  button_to(
43
43
  @url,
44
44
  method: @action.route_options.method,
45
- name: :return_to, value: request.original_url,
45
+ name: :return_to, value: (@action.return_to.nil? ? request.original_url : @action.return_to),
46
46
  class: "inline-block",
47
47
  form: {
48
48
  data: {
@@ -8,77 +8,30 @@ module Plutonium
8
8
  class ColorModeSelector < Plutonium::UI::Component::Base
9
9
  # Common CSS classes used across the component
10
10
  COMMON_CLASSES = {
11
- button: "w-full block py-2 px-4 text-sm text-gray-700 hover:bg-gray-100 dark:hover:text-white dark:text-gray-300 dark:hover:bg-gray-600",
12
- icon: "w-6 h-6 text-gray-800 dark:text-white",
13
- trigger: "inline-flex justify-center p-2 text-gray-500 rounded cursor-pointer dark:hover:text-white dark:text-gray-200 hover:text-gray-900 hover:bg-gray-100 dark:hover:bg-gray-600",
14
- dropdown: "hidden z-50 my-4 text-base list-none bg-white rounded divide-y divide-gray-100 shadow dark:bg-gray-700"
11
+ button: "inline-flex justify-center items-center p-2 text-gray-500 rounded cursor-pointer dark:hover:text-white dark:text-gray-200 hover:text-gray-900 hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors duration-200",
12
+ icon: "w-5 h-5"
15
13
  }.freeze
16
14
 
17
15
  # Available color modes with their associated icons and actions
18
16
  COLOR_MODES = [
19
- {label: "Light", icon: Phlex::TablerIcons::Sun, action: "setLightColorMode"},
20
- {label: "Dark", icon: Phlex::TablerIcons::Moon, action: "setDarkColorMode"},
21
- {label: "System", icon: Phlex::TablerIcons::DeviceDesktop, action: "setSystemColorMode"}
17
+ {mode: "light", icon: Phlex::TablerIcons::Sun, action: "setLightColorMode"},
18
+ {mode: "dark", icon: Phlex::TablerIcons::Moon, action: "setDarkColorMode"}
22
19
  ].freeze
23
20
 
24
21
  # Renders the color mode selector
25
22
  # @return [void]
26
23
  def view_template
27
- div(data_controller: "resource-drop-down") do
28
- render_dropdown_trigger
29
- render_dropdown_menu
30
- end
31
- end
32
-
33
- private
34
-
35
- # @private
36
- def render_dropdown_trigger
37
24
  button(
38
25
  type: "button",
39
- data_resource_drop_down_target: "trigger",
40
- class: COMMON_CLASSES[:trigger]
41
- ) do
42
- render Phlex::TablerIcons::Adjustments.new(class: COMMON_CLASSES[:icon])
43
- end
44
- end
45
-
46
- # @private
47
- def render_dropdown_menu
48
- div(
49
- class: COMMON_CLASSES[:dropdown],
50
- data_resource_drop_down_target: "menu"
26
+ class: COMMON_CLASSES[:button],
27
+ data_controller: "color-mode",
28
+ data_action: "click->color-mode#toggleMode",
29
+ data_color_mode_current_value: "light", # Default to light mode
30
+ title: "Toggle color mode"
51
31
  ) do
52
- render_color_mode_options
53
- end
54
- end
55
-
56
- # @private
57
- def render_color_mode_options
58
- ul(class: "py-1", role: "none") do
59
- COLOR_MODES.each do |mode|
60
- render_color_mode_button(**mode)
61
- end
62
- end
63
- end
64
-
65
- # @private
66
- # @param label [String] The text label for the button
67
- # @param icon [Class] The TablerIcon class to render
68
- # @param action [String] The color-mode controller action to trigger
69
- def render_color_mode_button(label:, icon:, action:)
70
- li do
71
- button(
72
- type: "button",
73
- class: COMMON_CLASSES[:button],
74
- role: "menuitem",
75
- data_action: "click->color-mode##{action}"
76
- ) do
77
- div(class: "flex justify-start") do
78
- render icon.new(class: COMMON_CLASSES[:icon])
79
- plain " #{label}"
80
- end
81
- end
32
+ # Both icons rendered, only one visible at a time
33
+ render Phlex::TablerIcons::Sun.new(class: "#{COMMON_CLASSES[:icon]} color-mode-icon-light", data: {color_mode_icon: "light"})
34
+ render Phlex::TablerIcons::Moon.new(class: "#{COMMON_CLASSES[:icon]} color-mode-icon-dark", data: {color_mode_icon: "dark"})
82
35
  end
83
36
  end
84
37
  end
@@ -56,6 +56,14 @@ module Plutonium
56
56
 
57
57
  private
58
58
 
59
+ # Renders the color mode toggle controls
60
+ # @private
61
+ def render_color_mode_controls
62
+ div(class: "bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700") do
63
+ render ColorModeSelector.new
64
+ end
65
+ end
66
+
59
67
  # Renders the left section of the header including sidebar toggle, brand elements,
60
68
  # and any yielded content
61
69
  # @private
@@ -111,8 +119,12 @@ module Plutonium
111
119
  # Renders the action buttons section
112
120
  # @private
113
121
  def render_actions
114
- div(class: "flex items-center lg:order-2") do
115
- action_slots.each { |action| render action }
122
+ div(class: "flex items-center space-x-2") do
123
+ render_color_mode_controls
124
+
125
+ div(class: "flex items-center lg:order-2") do
126
+ action_slots.each { |action| render action }
127
+ end
116
128
  end
117
129
  end
118
130
  end
@@ -15,7 +15,6 @@ module Plutonium
15
15
  def view_template(&)
16
16
  render_sidebar_container do
17
17
  render_content(&) if block_given?
18
- render_color_mode_controls
19
18
  end
20
19
  end
21
20
 
@@ -41,13 +40,6 @@ module Plutonium
41
40
  &
42
41
  )
43
42
  end
44
-
45
- # @private
46
- def render_color_mode_controls
47
- div(class: "absolute bottom-0 left-0 justify-center p-4 space-x-4 w-full flex bg-white dark:bg-gray-800 z-20 border-r border-gray-200 dark:border-gray-700") do
48
- render ColorModeSelector.new
49
- end
50
- end
51
43
  end
52
44
  end
53
45
  end
@@ -21,7 +21,7 @@ module Plutonium
21
21
  header_cell_sort_wrapper: "flex items-center",
22
22
  header_cell_sort_indicator: "ml-1.5",
23
23
  body_row: "bg-white border-b last:border-none dark:bg-gray-800 dark:border-gray-700",
24
- body_cell: "px-6 py-4 whitespace-pre max-w-[310px] hover:max-w-[600px] overflow-hidden text-ellipsis transition-all duration-300 ease-in-out",
24
+ body_cell: "px-6 py-4 whitespace-pre max-w-[450px] overflow-hidden text-ellipsis transition-all duration-300 ease-in-out",
25
25
  sort_icon: "w-3 h-3",
26
26
  sort_icon_active: "text-primary-600",
27
27
  sort_icon_inactive: "text-gray-600 dark:text-gray-500",
@@ -1,5 +1,5 @@
1
1
  module Plutonium
2
- VERSION = "0.26.4"
2
+ VERSION = "0.26.7"
3
3
  NEXT_MAJOR_VERSION = VERSION.split(".").tap { |v|
4
4
  v[1] = v[1].to_i + 1
5
5
  v[2] = 0
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@radioactive-labs/plutonium",
3
- "version": "0.4.10",
3
+ "version": "0.4.11",
4
4
  "description": "Core assets for the Plutonium gem",
5
5
  "type": "module",
6
6
  "main": "src/js/core.js",
@@ -1,40 +1,50 @@
1
- import { Controller } from "@hotwired/stimulus"
2
-
1
+ import { Controller } from "@hotwired/stimulus";
3
2
 
4
3
  // Connects to data-controller="color-mode"
5
4
  export default class extends Controller {
6
- // static targets = ["trigger", "menu"]
5
+ static values = { current: String };
7
6
 
8
7
  connect() {
9
- this.updateColorMode()
8
+ // Set initial mode from localStorage or default
9
+ const mode = localStorage.theme || "light";
10
+ this.setMode(mode);
10
11
  }
11
12
 
12
- disconnect() {
13
+ toggleMode() {
14
+ const current = this.currentValue || "light";
15
+ const next = current === "light" ? "dark" : "light";
16
+ this.setMode(next);
13
17
  }
14
18
 
15
- updateColorMode() {
16
- if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
17
- document.documentElement.classList.add('dark')
19
+ setMode(mode) {
20
+ // Update html class
21
+ if (mode === "dark") {
22
+ document.documentElement.classList.add("dark");
23
+ localStorage.theme = "dark";
18
24
  } else {
19
- document.documentElement.classList.remove('dark')
25
+ document.documentElement.classList.remove("dark");
26
+ localStorage.theme = "light";
20
27
  }
21
- }
22
28
 
23
- setLightColorMode() {
24
- // Whenever the user explicitly chooses light mode
25
- localStorage.theme = 'light'
26
- this.updateColorMode()
27
- }
29
+ // Update button state
30
+ this.currentValue = mode;
28
31
 
29
- setDarkColorMode() {
30
- // Whenever the user explicitly chooses dark mode
31
- localStorage.theme = 'dark'
32
- this.updateColorMode()
32
+ // Show/hide icons
33
+ this.toggleIcons(mode);
33
34
  }
34
35
 
35
- setSystemColorMode() {
36
- // Whenever the user explicitly chooses to respect the OS preference
37
- localStorage.removeItem('theme')
38
- this.updateColorMode()
36
+ toggleIcons(mode) {
37
+ const sun = this.element.querySelector(".color-mode-icon-light");
38
+ const moon = this.element.querySelector(".color-mode-icon-dark");
39
+
40
+ if (sun && moon) {
41
+ if (mode === "light") {
42
+ sun.classList.remove("hidden");
43
+ moon.classList.add("hidden");
44
+ } else {
45
+ sun.classList.add("hidden");
46
+ moon.classList.remove("hidden");
47
+ }
48
+ }
39
49
  }
40
50
  }
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: plutonium
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.26.4
4
+ version: 0.26.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Froelich
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-07-30 00:00:00.000000000 Z
11
+ date: 2025-08-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: zeitwerk
@@ -486,6 +486,7 @@ files:
486
486
  - app/views/rodauth/webauthn_setup.html.erb
487
487
  - brakeman.ignore
488
488
  - config.ru
489
+ - config/initializers/action_policy.rb
489
490
  - config/initializers/hotwire_turbo_monkey_patches.rb
490
491
  - config/initializers/pagy.rb
491
492
  - config/initializers/rabl.rb
@@ -735,6 +736,7 @@ files:
735
736
  - lib/plutonium/action/interactive.rb
736
737
  - lib/plutonium/action/route_options.rb
737
738
  - lib/plutonium/action/simple.rb
739
+ - lib/plutonium/action_policy/sti_policy_lookup.rb
738
740
  - lib/plutonium/auth.rb
739
741
  - lib/plutonium/auth/public.rb
740
742
  - lib/plutonium/auth/rodauth.rb