modal_stack 0.1.1 → 0.3.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.
@@ -11,14 +11,17 @@ module ModalStack
11
11
  # end
12
12
  #
13
13
  class Configuration
14
- CSS_PROVIDERS = %i[tailwind bootstrap vanilla none].freeze
14
+ CSS_PROVIDERS = %i[tailwind_v3 tailwind_v4 bootstrap vanilla none].freeze
15
+ # Aliases accepted on input, normalized to a canonical CSS_PROVIDERS value.
16
+ # `:tailwind` predates the v3/v4 split — keep it working, map to v3 (no change
17
+ # in rendered CSS for existing apps).
18
+ CSS_PROVIDER_ALIASES = { tailwind: :tailwind_v3 }.freeze
15
19
  ASSETS_MODES = %i[importmap jsbundling sprockets auto].freeze
16
20
  VARIANTS = %i[modal drawer bottom_sheet confirmation].freeze
17
21
  SIZES = %i[sm md lg xl].freeze
22
+ MAX_DEPTH_STRATEGIES = %i[raise warn silent].freeze
18
23
 
19
24
  attr_accessor :default_classes,
20
- :default_dismissible,
21
- :max_depth,
22
25
  :request_header,
23
26
  :dialog_id,
24
27
  :stack_root_data_attribute,
@@ -28,15 +31,22 @@ module ModalStack
28
31
  :initializer_version,
29
32
  :silence_initializer_warning
30
33
 
31
- attr_reader :css_provider, :assets_mode, :default_variant, :default_size
34
+ attr_reader :css_provider,
35
+ :assets_mode,
36
+ :default_variant,
37
+ :default_size,
38
+ :default_dismissible,
39
+ :max_depth,
40
+ :max_depth_strategy
32
41
 
33
42
  def initialize
34
- @css_provider = :tailwind
43
+ @css_provider = :tailwind_v3
35
44
  @assets_mode = :auto
36
45
  @default_variant = :modal
37
46
  @default_size = :md
38
47
  @default_dismissible = true
39
48
  @max_depth = 5
49
+ @max_depth_strategy = :warn
40
50
  @request_header = "X-Modal-Stack-Request"
41
51
  @dialog_id = "modal-stack-root"
42
52
  @stack_root_data_attribute = "modal-stack"
@@ -50,6 +60,7 @@ module ModalStack
50
60
 
51
61
  def css_provider=(value)
52
62
  value = value.to_sym
63
+ value = CSS_PROVIDER_ALIASES.fetch(value, value)
53
64
  raise ArgumentError, "css_provider must be one of #{CSS_PROVIDERS.inspect}, got #{value.inspect}" unless CSS_PROVIDERS.include?(value)
54
65
 
55
66
  @css_provider = value
@@ -76,6 +87,34 @@ module ModalStack
76
87
  @default_size = value
77
88
  end
78
89
 
90
+ def default_dismissible=(value)
91
+ raise ArgumentError, "default_dismissible must be true or false, got #{value.inspect}" unless [true, false].include?(value)
92
+
93
+ @default_dismissible = value
94
+ end
95
+
96
+ def max_depth=(value)
97
+ if value.nil?
98
+ @max_depth = nil
99
+ return
100
+ end
101
+
102
+ coerced = Integer(value, exception: false)
103
+ raise ArgumentError, "max_depth must be a positive integer or nil, got #{value.inspect}" if coerced.nil? || coerced < 1
104
+
105
+ @max_depth = coerced
106
+ end
107
+
108
+ def max_depth_strategy=(value)
109
+ value = value.to_sym
110
+ unless MAX_DEPTH_STRATEGIES.include?(value)
111
+ raise ArgumentError,
112
+ "max_depth_strategy must be one of #{MAX_DEPTH_STRATEGIES.inspect}, got #{value.inspect}"
113
+ end
114
+
115
+ @max_depth_strategy = value
116
+ end
117
+
79
118
  private
80
119
 
81
120
  def default_classes_hash
@@ -5,7 +5,7 @@ module ModalStack
5
5
  extend ActiveSupport::Concern
6
6
 
7
7
  included do
8
- helper_method :modal_stack_request?
8
+ helper_method :modal_stack_request?, :modal_stack_config
9
9
  end
10
10
 
11
11
  class_methods do
@@ -39,6 +39,13 @@ module ModalStack
39
39
  end
40
40
  end
41
41
 
42
+ # Request-scoped accessor for `ModalStack.configuration`. Memoized so
43
+ # helpers that read several config values per render hit the global
44
+ # singleton once instead of N times.
45
+ def modal_stack_config
46
+ @modal_stack_config ||= ModalStack.configuration
47
+ end
48
+
42
49
  # True when the current request was issued by the modal_stack JS runtime
43
50
  # (signaled by the X-Modal-Stack-Request header on the fetch).
44
51
  def modal_stack_request?
@@ -11,7 +11,7 @@ module ModalStack
11
11
  # Returns an empty SafeBuffer when `config.css_provider = :none`,
12
12
  # so apps can call this unconditionally.
13
13
  def modal_stack_stylesheet_link_tag(**)
14
- provider = ModalStack.configuration.css_provider
14
+ provider = _modal_stack_config.css_provider
15
15
  return ActiveSupport::SafeBuffer.new if provider == :none
16
16
 
17
17
  stylesheet_link_tag("modal_stack/#{provider}", **)
@@ -23,23 +23,43 @@ module ModalStack
23
23
  # <%= modal_stack_dialog_tag %>
24
24
  #
25
25
  def modal_stack_dialog_tag(**html_options)
26
- config = ModalStack.configuration
26
+ config = _modal_stack_config
27
27
  attrs = html_options.dup
28
28
  attrs[:id] ||= config.dialog_id
29
-
30
- existing_data = attrs[:data] || {}
31
- controllers = [existing_data[:controller], config.stack_root_data_attribute].compact.join(" ").strip
32
- attrs[:data] = existing_data.merge(controller: controllers)
29
+ attrs[:data] = build_dialog_data(attrs[:data], config)
33
30
 
34
31
  content_tag(:dialog, "".html_safe, attrs)
35
32
  end
36
33
 
34
+ # Merges caller-provided data attrs with the gem-managed ones (controller,
35
+ # max-depth value/strategy). Caller data wins on key collision.
36
+ def build_dialog_data(provided, config)
37
+ existing = provided || {}
38
+ controllers = [existing[:controller], config.stack_root_data_attribute].compact.join(" ").strip
39
+ data = existing.merge(controller: controllers)
40
+ data[:modal_stack_max_depth_value] ||= config.max_depth if config.max_depth
41
+ data[:modal_stack_max_depth_strategy_value] ||= config.max_depth_strategy.to_s
42
+ data
43
+ end
44
+
37
45
  # Emits a no-op SafeBuffer for now — kept as a stable hook for apps
38
46
  # that prefer a single line in their layout. The actual JS loading
39
47
  # is handled by the host app's bundler / importmap.
40
48
  def modal_stack_javascript_tag(**)
41
49
  ActiveSupport::SafeBuffer.new
42
50
  end
51
+
52
+ private
53
+
54
+ # Prefers the request-scoped accessor injected by ControllerExtensions
55
+ # so we hit `ModalStack.configuration` once per request, not per call.
56
+ # Falls back to the global singleton for non-controller render contexts
57
+ # (mailers, ActionCable, isolated view tests).
58
+ def _modal_stack_config
59
+ return modal_stack_config if respond_to?(:modal_stack_config, true)
60
+
61
+ ModalStack.configuration
62
+ end
43
63
  end
44
64
  end
45
65
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ModalStack
4
- VERSION = "0.1.1"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/modal_stack.rb CHANGED
@@ -14,7 +14,7 @@ module ModalStack
14
14
  # Bumped when config/initializers/modal_stack.rb gains/loses an option,
15
15
  # so apps that haven't regenerated their initializer get a one-line
16
16
  # boot warning. Independent from the gem's VERSION.
17
- INITIALIZER_VERSION = "0.1.0"
17
+ INITIALIZER_VERSION = "0.3.0"
18
18
 
19
19
  class << self
20
20
  def configuration
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: modal_stack
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Florian Gagnaire
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-05-02 00:00:00.000000000 Z
11
+ date: 2026-05-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: railties
@@ -70,7 +70,8 @@ files:
70
70
  - Rakefile
71
71
  - app/assets/javascripts/modal_stack.js
72
72
  - app/assets/stylesheets/modal_stack/bootstrap.css
73
- - app/assets/stylesheets/modal_stack/tailwind.css
73
+ - app/assets/stylesheets/modal_stack/tailwind_v3.css
74
+ - app/assets/stylesheets/modal_stack/tailwind_v4.css
74
75
  - app/assets/stylesheets/modal_stack/vanilla.css
75
76
  - app/javascript/modal_stack/controllers/modal_stack_controller.js
76
77
  - app/javascript/modal_stack/controllers/modal_stack_link_controller.js