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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +43 -0
- data/README.md +73 -26
- data/app/assets/javascripts/modal_stack.js +230 -41
- data/app/assets/stylesheets/modal_stack/bootstrap.css +7 -8
- data/app/assets/stylesheets/modal_stack/{tailwind.css → tailwind_v3.css} +19 -12
- data/app/assets/stylesheets/modal_stack/tailwind_v4.css +311 -0
- data/app/assets/stylesheets/modal_stack/vanilla.css +7 -8
- data/app/javascript/modal_stack/controllers/modal_stack_controller.js +52 -13
- data/app/javascript/modal_stack/controllers/modal_stack_link_controller.js +29 -7
- data/app/javascript/modal_stack/orchestrator.js +136 -4
- data/app/javascript/modal_stack/orchestrator.test.js +218 -2
- data/app/javascript/modal_stack/runtime.js +91 -10
- data/app/javascript/modal_stack/runtime.test.js +138 -1
- data/app/javascript/modal_stack/state.js +142 -8
- data/app/javascript/modal_stack/state.test.js +89 -5
- data/lib/generators/modal_stack/install/install_generator.rb +18 -4
- data/lib/generators/modal_stack/install/templates/initializer.rb +19 -6
- data/lib/modal_stack/configuration.rb +44 -5
- data/lib/modal_stack/controller_extensions.rb +8 -1
- data/lib/modal_stack/helpers/modal_stack_assets_helper.rb +26 -6
- data/lib/modal_stack/version.rb +1 -1
- data/lib/modal_stack.rb +1 -1
- metadata +4 -3
|
@@ -11,14 +11,17 @@ module ModalStack
|
|
|
11
11
|
# end
|
|
12
12
|
#
|
|
13
13
|
class Configuration
|
|
14
|
-
CSS_PROVIDERS = %i[
|
|
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,
|
|
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 = :
|
|
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 =
|
|
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 =
|
|
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
|
data/lib/modal_stack/version.rb
CHANGED
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.
|
|
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.
|
|
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-
|
|
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/
|
|
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
|